use std::env;

use chrono::{DateTime, Days, Months, TimeDelta};
use chrono_tz::Tz;
use lazy_static::lazy_static;
use log::{error, info, warn};
use num_integer::Integer;
use poise::serenity_prelude::{
    all::{CreateAttachment, CreateEmbedFooter},
    builder::{CreateEmbed, CreateEmbedAuthor, CreateMessage, ExecuteWebhook},
    http::{CacheHttp, Http, HttpError},
    model::{
        channel::Channel,
        id::{ChannelId, MessageId},
        webhook::Webhook,
    },
    Error, Result,
};
use regex::{Captures, Regex};
use serde::Deserialize;
use sqlx::{
    types::{
        chrono::{NaiveDateTime, Utc},
        Json,
    },
    Executor,
};

use crate::{
    metrics::{REMINDER_COUNTER, REMINDER_FAIL_COUNTER},
    Database,
};

lazy_static! {
    pub static ref TIMEFROM_REGEX: Regex =
        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
    pub static ref TIMENOW_REGEX: Regex = Regex::new(
        r#"<<timenow(?:(?P<sign>[+-])(?P<offset>\d+))?:(?P<timezone>(?:\w|/|_)+?):(?P<format>.+?)?>>"#
    )
    .unwrap();
    pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
}

fn fmt_displacement(format: &str, seconds: u64) -> String {
    let mut seconds = seconds;
    let mut days: u64 = 0;
    let mut hours: u64 = 0;
    let mut minutes: u64 = 0;

    for (rep, time_type, div) in
        [("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
    {
        if format.contains(*rep) {
            let (divided, new_seconds) = seconds.div_rem(&div);

            **time_type = divided;
            seconds = new_seconds;
        }
    }

    format
        .replace("%s", &seconds.to_string())
        .replace("%m", &minutes.to_string())
        .replace("%h", &hours.to_string())
        .replace("%d", &days.to_string())
}

pub fn substitute(string: &str) -> String {
    let new = TIMEFROM_REGEX.replace_all(string, |caps: &Captures| {
        let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
        let format = caps.name("format").map(|m| m.as_str());

        if let (Some(final_time), Some(format)) = (final_time, format) {
            match DateTime::from_timestamp(final_time, 0) {
                Some(dt) => {
                    let now = Utc::now();

                    let difference = {
                        if now < dt {
                            dt - Utc::now()
                        } else {
                            Utc::now() - dt
                        }
                    };

                    fmt_displacement(format, difference.num_seconds() as u64)
                }

                None => String::new(),
            }
        } else {
            String::new()
        }
    });

    TIMENOW_REGEX
        .replace_all(&new, |caps: &Captures| {
            let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
            let format = caps.name("format").map(|m| m.as_str());
            let sign = caps.name("sign").map(|m| m.as_str());
            let offset = caps.name("offset").map(|m| m.as_str().parse::<i64>().ok()).flatten();

            if let (Some(timezone), Some(format)) = (timezone, format) {
                let mut now = Utc::now().with_timezone(&timezone);
                if let (Some(sign), Some(offset)) = (sign, offset) {
                    now = now
                        .checked_add_signed(TimeDelta::seconds(
                            offset * {
                                match sign {
                                    "-" => -1,
                                    _ => 1,
                                }
                            },
                        ))
                        .unwrap_or(now)
                }

                now.format(format).to_string()
            } else {
                String::new()
            }
        })
        .to_string()
}

struct Embed {
    title: String,
    description: String,
    image_url: Option<String>,
    thumbnail_url: Option<String>,
    footer: String,
    footer_url: Option<String>,
    author: String,
    author_url: Option<String>,
    color: u32,
    fields: Json<Vec<EmbedField>>,
}

#[derive(Deserialize)]
struct EmbedField {
    title: String,
    value: String,
    inline: bool,
}

impl Embed {
    pub async fn from_id(
        pool: impl Executor<'_, Database = Database> + Copy,
        id: u32,
    ) -> Option<Self> {
        match sqlx::query_as!(
            Self,
            r#"
            SELECT
             `embed_title` AS title,
             `embed_description` AS description,
             `embed_image_url` AS image_url,
             `embed_thumbnail_url` AS thumbnail_url,
             `embed_footer` AS footer,
             `embed_footer_url` AS footer_url,
             `embed_author` AS author,
             `embed_author_url` AS author_url,
             `embed_color` AS color,
             IFNULL(`embed_fields`, '[]') AS "fields:_"
            FROM reminders
            WHERE `id` = ?"#,
            id
        )
        .fetch_one(pool)
        .await
        {
            Ok(mut embed) => {
                embed.title = substitute(&embed.title);
                embed.description = substitute(&embed.description);
                embed.footer = substitute(&embed.footer);

                embed.fields.iter_mut().for_each(|field| {
                    field.title = substitute(&field.title);
                    field.value = substitute(&field.value);
                });

                if embed.has_content() {
                    Some(embed)
                } else {
                    None
                }
            }

            Err(e) => {
                warn!("Error loading embed from reminder: {:?}", e);

                None
            }
        }
    }

    pub fn has_content(&self) -> bool {
        if self.title.is_empty()
            && self.description.is_empty()
            && self.image_url.is_none()
            && self.thumbnail_url.is_none()
            && self.footer.is_empty()
            && self.footer_url.is_none()
            && self.author.is_empty()
            && self.author_url.is_none()
            && self.fields.0.is_empty()
        {
            false
        } else {
            true
        }
    }
}

impl Into<CreateEmbed> for Embed {
    fn into(self) -> CreateEmbed {
        let mut author = CreateEmbedAuthor::new(&self.author);
        if let Some(author_icon) = &self.author_url {
            author = author.icon_url(author_icon);
        }

        let mut footer = CreateEmbedFooter::new(&self.footer);
        if let Some(footer_icon) = &self.footer_url {
            footer = footer.icon_url(footer_icon);
        }

        let mut embed = CreateEmbed::default()
            .title(&self.title)
            .description(&self.description)
            .color(self.color)
            .author(author)
            .footer(footer);

        for field in &self.fields.0 {
            embed = embed.field(&field.title, &field.value, field.inline);
        }

        if let Some(image_url) = &self.image_url {
            embed = embed.image(image_url);
        }

        if let Some(thumbnail_url) = &self.thumbnail_url {
            embed = embed.thumbnail(thumbnail_url);
        }

        embed
    }
}

pub struct Reminder {
    id: u32,

    channel_id: u64,
    thread_id: Option<u64>,
    webhook_id: Option<u64>,
    webhook_token: Option<String>,

    channel_paused: bool,
    channel_paused_until: Option<NaiveDateTime>,
    enabled: bool,

    tts: bool,
    pin: bool,
    content: String,
    attachment: Option<Vec<u8>>,
    attachment_name: Option<String>,

    utc_time: DateTime<Utc>,
    timezone: String,
    restartable: bool,
    expires: Option<DateTime<Utc>>,
    interval_seconds: Option<u32>,
    interval_days: Option<u32>,
    interval_months: Option<u32>,

    avatar: Option<String>,
    username: Option<String>,
}

impl Reminder {
    pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> {
        match sqlx::query_as_unchecked!(
            Reminder,
            r#"
            SELECT
                reminders.`id` AS id,

                channels.`channel` AS channel_id,
                reminders.`thread_id` AS thread_id,
                channels.`webhook_id` AS webhook_id,
                channels.`webhook_token` AS webhook_token,

                channels.`paused` AS 'channel_paused',
                channels.`paused_until` AS 'channel_paused_until',
                reminders.`enabled` AS 'enabled',

                reminders.`tts` AS tts,
                reminders.`pin` AS pin,
                reminders.`content` AS content,
                reminders.`attachment` AS attachment,
                reminders.`attachment_name` AS attachment_name,

                reminders.`utc_time` AS 'utc_time',
                reminders.`timezone` AS timezone,
                reminders.`restartable` AS restartable,
                reminders.`expires` AS 'expires',
                reminders.`interval_seconds` AS 'interval_seconds',
                reminders.`interval_days` AS 'interval_days',
                reminders.`interval_months` AS 'interval_months',

                reminders.`avatar` AS avatar,
                reminders.`username` AS username
            FROM
                reminders
            INNER JOIN
                channels
            ON
                reminders.channel_id = channels.id
            WHERE
                reminders.`status` = 'pending' AND
                reminders.`id` IN (
                    SELECT
                        MIN(id)
                    FROM
                        reminders
                    WHERE
                        reminders.`utc_time` <= NOW() AND
                        `status` = 'pending' AND
                        (
                            reminders.`interval_seconds` IS NOT NULL
                            OR reminders.`interval_months` IS NOT NULL
                            OR reminders.`interval_days` IS NOT NULL
                            OR reminders.enabled
                        )
                    GROUP BY channel_id
                )
            "#,
        )
        .fetch_all(pool)
        .await
        {
            Ok(reminders) => reminders
                .into_iter()
                .map(|mut rem| {
                    rem.content = substitute(&rem.content);

                    rem
                })
                .collect::<Vec<Self>>(),

            Err(e) => {
                warn!("Could not fetch reminders: {:?}", e);

                vec![]
            }
        }
    }

    async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
        let _ = sqlx::query!(
            "
            UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
            ",
            self.channel_id
        )
        .execute(pool)
        .await;
    }

    async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) {
        if self.interval_seconds.is_some()
            || self.interval_months.is_some()
            || self.interval_days.is_some()
        {
            // If all intervals are zero then dont care
            if self.interval_seconds == Some(0)
                && self.interval_days == Some(0)
                && self.interval_months == Some(0)
            {
                self.set_sent(pool).await;
            }

            let now = Utc::now();
            let mut updated_reminder_time =
                self.utc_time.with_timezone(&self.timezone.parse().unwrap_or(Tz::UTC));
            let mut fail_count = 0;

            while updated_reminder_time < now && fail_count < 4 {
                if let Some(interval) = self.interval_months {
                    if interval != 0 {
                        updated_reminder_time = updated_reminder_time
                            .checked_add_months(Months::new(interval))
                            .unwrap_or_else(|| {
                                warn!(
                                    "{}: Could not add {} months to a reminder",
                                    interval, self.id
                                );
                                fail_count += 1;

                                updated_reminder_time
                            });
                    }
                }

                if let Some(interval) = self.interval_days {
                    if interval != 0 {
                        updated_reminder_time = updated_reminder_time
                            .checked_add_days(Days::new(interval as u64))
                            .unwrap_or_else(|| {
                                warn!("{}: Could not add {} days to a reminder", self.id, interval);
                                fail_count += 1;

                                updated_reminder_time
                            })
                    }
                }

                if let Some(interval) = self.interval_seconds {
                    updated_reminder_time += TimeDelta::try_seconds(interval as i64)
                        .unwrap_or_else(|| {
                            warn!("{}: Could not add {} seconds to a reminder", self.id, interval);
                            fail_count += 1;

                            TimeDelta::zero()
                        });
                }
            }

            if fail_count >= 4 {
                self.log_error(
                    "Failed to update 4 times and so is being deleted",
                    None::<&'static str>,
                )
                .await;
                self.set_failed(pool, "Failed to update 4 times and so is being deleted").await;
            } else if self.expires.map_or(false, |expires| updated_reminder_time > expires) {
                self.set_sent(pool).await;
            } else {
                sqlx::query!(
                    "UPDATE reminders SET `utc_time` = ? WHERE `id` = ?",
                    updated_reminder_time.with_timezone(&Utc),
                    self.id
                )
                .execute(pool)
                .await
                .expect(&format!("Could not update time on Reminder {}", self.id));
            }
        } else {
            self.set_sent(pool).await;
        }
    }

    async fn log_error(&self, error: &'static str, debug_info: Option<impl std::fmt::Debug>) {
        let message = match debug_info {
            Some(info) => format!(
                "{}
{:?}",
                error, info
            ),

            None => error.to_string(),
        };

        REMINDER_FAIL_COUNTER
            .get_metric_with_label_values(&[
                self.id.to_string().as_str(),
                self.channel_id.to_string().as_str(),
                &message,
            ])
            .map_or_else(|e| warn!("Couldn't count failed reminder: {:?}", e), |c| c.inc());
        error!("[Reminder {}] {}", self.id, message);
    }

    async fn log_success(&self) {
        REMINDER_COUNTER
            .get_metric_with_label_values(&[
                self.id.to_string().as_str(),
                self.channel_id.to_string().as_str(),
            ])
            .map_or_else(|e| warn!("Couldn't count sent reminder: {:?}", e), |c| c.inc());
    }

    async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) {
        sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id)
            .execute(pool)
            .await
            .expect(&format!("Could not delete Reminder {}", self.id));
    }

    async fn set_failed(
        &self,
        pool: impl Executor<'_, Database = Database> + Copy,
        message: &'static str,
    ) {
        sqlx::query!(
            "UPDATE reminders SET `status` = 'failed', `status_message` = ? WHERE `id` = ?",
            message,
            self.id
        )
        .execute(pool)
        .await
        .expect(&format!("Could not delete Reminder {}", self.id));
    }

    async fn pin_message<M: Into<MessageId>>(&self, message_id: M, http: impl AsRef<Http>) {
        let _ = http.as_ref().pin_message(self.channel_id.into(), message_id.into(), None).await;
    }

    pub async fn send(
        &self,
        pool: impl Executor<'_, Database = Database> + Copy,
        cache_http: impl CacheHttp,
    ) {
        async fn send_to_channel(
            cache_http: impl CacheHttp,
            reminder: &Reminder,
            embed: Option<CreateEmbed>,
        ) -> Result<()> {
            let channel = if let Some(thread_id) = reminder.thread_id {
                ChannelId::new(thread_id).to_channel(&cache_http).await
            } else {
                ChannelId::new(reminder.channel_id).to_channel(&cache_http).await
            };

            let mut message = CreateMessage::new().content(&reminder.content).tts(reminder.tts);

            if let (Some(attachment), Some(name)) =
                (&reminder.attachment, &reminder.attachment_name)
            {
                message =
                    message.add_file(CreateAttachment::bytes(attachment as &[u8], name.as_str()));
            }

            if let Some(embed) = embed {
                message = message.embed(embed);
            }

            match channel {
                Ok(Channel::Guild(channel)) => {
                    match channel.send_message(&cache_http, message).await {
                        Ok(m) => {
                            if reminder.pin {
                                reminder.pin_message(m.id, cache_http.http()).await;
                            }

                            Ok(())
                        }
                        Err(e) => Err(e),
                    }
                }
                Ok(Channel::Private(channel)) => {
                    match channel.send_message(&cache_http.http(), message).await {
                        Ok(m) => {
                            if reminder.pin {
                                reminder.pin_message(m.id, cache_http.http()).await;
                            }

                            Ok(())
                        }
                        Err(e) => Err(e),
                    }
                }
                Err(e) => Err(e),
                _ => Err(Error::Other("Channel not of valid type")),
            }
        }

        async fn send_to_webhook(
            cache_http: impl CacheHttp,
            reminder: &Reminder,
            webhook: Webhook,
            embed: Option<CreateEmbed>,
        ) -> Result<()> {
            let mut builder = if let Some(thread_id) = reminder.thread_id {
                ExecuteWebhook::new()
                    .content(&reminder.content)
                    .tts(reminder.tts)
                    .in_thread(thread_id)
            } else {
                ExecuteWebhook::new().content(&reminder.content).tts(reminder.tts)
            };

            if let Some(username) = &reminder.username {
                if !username.is_empty() {
                    builder = builder.username(username);
                }
            }

            if let Some(avatar) = &reminder.avatar {
                builder = builder.avatar_url(avatar);
            }

            if let (Some(attachment), Some(name)) =
                (&reminder.attachment, &reminder.attachment_name)
            {
                builder =
                    builder.add_file(CreateAttachment::bytes(attachment as &[u8], name.as_str()));
            }

            if let Some(embed) = embed {
                builder = builder.embeds(vec![embed]);
            }

            match webhook
                .execute(&cache_http.http(), reminder.pin || reminder.restartable, builder)
                .await
            {
                Ok(m) => {
                    if reminder.pin {
                        if let Some(message) = m {
                            reminder.pin_message(message.id, cache_http.http()).await;
                        }
                    }

                    Ok(())
                }
                Err(e) => Err(e),
            }
        }

        if self.enabled
            && !(self.channel_paused
                && self
                    .channel_paused_until
                    .map_or(true, |inner| inner >= Utc::now().naive_local()))
        {
            let _ = sqlx::query!(
                "
                UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
                ",
                self.channel_id
            )
            .execute(pool)
            .await;

            let embed = Embed::from_id(pool, self.id).await.map(|e| e.into());

            let result = if let (Some(webhook_id), Some(webhook_token)) =
                (self.webhook_id, &self.webhook_token)
            {
                let webhook_res = cache_http
                    .http()
                    .get_webhook_with_token(webhook_id.into(), webhook_token)
                    .await;

                if let Ok(webhook) = webhook_res {
                    send_to_webhook(cache_http, &self, webhook, embed).await
                } else {
                    warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res);

                    self.reset_webhook(pool).await;
                    send_to_channel(cache_http, &self, embed).await
                }
            } else {
                send_to_channel(cache_http, &self, embed).await
            };

            if let Err(e) = result {
                if let Error::Http(error) = e {
                    if let HttpError::UnsuccessfulRequest(http_error) = error {
                        match http_error.error.code {
                            10003 => {
                                self.log_error(
                                    "Could not be sent as channel does not exist",
                                    None::<&'static str>,
                                )
                                .await;
                                self.set_failed(
                                    pool,
                                    "Could not be sent as channel does not exist",
                                )
                                .await;
                            }
                            10004 => {
                                self.log_error(
                                    "Could not be sent as guild does not exist",
                                    None::<&'static str>,
                                )
                                .await;
                                self.set_failed(pool, "Could not be sent as guild does not exist")
                                    .await;
                            }
                            50001 => {
                                self.log_error(
                                    "Could not be sent as missing access",
                                    None::<&'static str>,
                                )
                                .await;
                                self.set_failed(pool, "Could not be sent as missing access").await;
                            }
                            50007 => {
                                self.log_error(
                                    "Could not be sent as user has DMs disabled",
                                    None::<&'static str>,
                                )
                                .await;
                                self.set_failed(pool, "Could not be sent as user has DMs disabled")
                                    .await;
                            }
                            50013 => {
                                self.log_error(
                                    "Could not be sent as permissions are invalid",
                                    None::<&'static str>,
                                )
                                .await;
                                self.set_failed(
                                    pool,
                                    "Could not be sent as permissions are invalid",
                                )
                                .await;
                            }
                            _ => {
                                self.log_error("HTTP error sending reminder", Some(http_error))
                                    .await;
                                self.refresh(pool).await;
                            }
                        }
                    } else {
                        self.log_error("(Likely) a parsing error", Some(error)).await;
                        self.refresh(pool).await;
                    }
                } else {
                    self.log_error("Non-HTTP error", Some(e)).await;
                    self.refresh(pool).await;
                }
            } else {
                self.log_success().await;
                self.refresh(pool).await;
            }
        } else {
            info!("Reminder {} is paused", self.id);

            self.refresh(pool).await;
        }
    }
}
