use std::path::Path;

use base64::{prelude::BASE64_STANDARD, Engine};
use chrono::{naive::NaiveDateTime, Utc};
use log::warn;
use rand::{rngs::OsRng, seq::IteratorRandom};
use rocket::{
    fs::NamedFile, get, http::CookieJar, response::Redirect, serde::json::json, Responder,
};
use rocket_dyn_templates::Template;
use secrecy::ExposeSecret;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serenity::http::HttpError;
use serenity::{
    all::CacheHttp,
    builder::CreateWebhook,
    client::Context,
    model::id::{ChannelId, GuildId, UserId},
};
use sqlx::types::Json;
use sqlx::FromRow;

use crate::utils::check_subscription;
use crate::web::{
    catchers::internal_server_error,
    consts::{
        CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH,
        MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH,
        MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH,
        MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL,
    },
    guards::transaction::Transaction,
    routes::JsonResult,
    string, Error,
};

pub mod api;
pub mod export;

type Unset<T> = Option<T>;

fn name_default() -> String {
    "Reminder".to_string()
}

fn template_name_default() -> String {
    "Template".to_string()
}

fn channel_default() -> u64 {
    0
}

fn id_default() -> u32 {
    0
}

fn interval_default() -> Unset<Option<u32>> {
    None
}

#[derive(sqlx::Type)]
#[sqlx(transparent)]
pub struct Attachment(Vec<u8>);

impl<'de> Deserialize<'de> for Attachment {
    fn deserialize<D>(deserializer: D) -> Result<Attachment, D::Error>
    where
        D: Deserializer<'de>,
    {
        let string = String::deserialize(deserializer)?;
        Ok(Attachment(BASE64_STANDARD.decode(string).map_err(de::Error::custom)?))
    }
}

impl Serialize for Attachment {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(&BASE64_STANDARD.encode(&self.0))
    }
}

#[derive(Serialize, Deserialize)]
pub struct ReminderTemplate {
    #[serde(default = "id_default")]
    id: u32,
    #[serde(default = "id_default")]
    guild_id: u32,
    #[serde(default = "template_name_default")]
    name: String,
    attachment: Option<Attachment>,
    attachment_name: Option<String>,
    avatar: Option<String>,
    content: String,
    embed_author: String,
    embed_author_url: Option<String>,
    embed_color: u32,
    embed_description: String,
    embed_footer: String,
    embed_footer_url: Option<String>,
    embed_image_url: Option<String>,
    embed_thumbnail_url: Option<String>,
    embed_title: String,
    embed_fields: Option<Json<Vec<EmbedField>>>,
    interval_seconds: Option<u32>,
    interval_days: Option<u32>,
    interval_months: Option<u32>,
    tts: bool,
    username: Option<String>,
}

#[derive(Serialize, Deserialize)]
pub struct ReminderTemplateCsv {
    #[serde(default = "template_name_default")]
    name: String,
    attachment: Option<Attachment>,
    attachment_name: Option<String>,
    avatar: Option<String>,
    content: String,
    embed_author: String,
    embed_author_url: Option<String>,
    embed_color: u32,
    embed_description: String,
    embed_footer: String,
    embed_footer_url: Option<String>,
    embed_image_url: Option<String>,
    embed_thumbnail_url: Option<String>,
    embed_title: String,
    embed_fields: Option<String>,
    interval_seconds: Option<u32>,
    interval_days: Option<u32>,
    interval_months: Option<u32>,
    tts: bool,
    username: Option<String>,
}

#[derive(Deserialize)]
pub struct DeleteReminderTemplate {
    id: u32,
}

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

#[derive(Deserialize)]
pub struct CreateReminder {
    attachment: Option<Attachment>,
    attachment_name: Option<String>,
    avatar: Option<String>,
    #[serde(with = "string")]
    channel: u64,
    content: String,
    embed_author: String,
    embed_author_url: Option<String>,
    embed_color: u32,
    embed_description: String,
    embed_footer: String,
    embed_footer_url: Option<String>,
    embed_image_url: Option<String>,
    embed_thumbnail_url: Option<String>,
    embed_title: String,
    embed_fields: Option<Json<Vec<EmbedField>>>,
    enabled: bool,
    expires: Option<NaiveDateTime>,
    interval_seconds: Option<u32>,
    interval_days: Option<u32>,
    interval_months: Option<u32>,
    #[serde(default = "name_default")]
    name: String,
    restartable: bool,
    tts: bool,
    username: Option<String>,
    utc_time: NaiveDateTime,
}

#[derive(Serialize, FromRow)]
pub struct GetReminder {
    attachment_name: Option<String>,
    avatar: Option<String>,
    #[serde(with = "string")]
    channel: u64,
    content: String,
    embed_author: String,
    embed_author_url: Option<String>,
    embed_color: u32,
    embed_description: String,
    embed_footer: String,
    embed_footer_url: Option<String>,
    embed_image_url: Option<String>,
    embed_thumbnail_url: Option<String>,
    embed_title: String,
    embed_fields: Option<Json<Vec<EmbedField>>>,
    enabled: bool,
    expires: Option<NaiveDateTime>,
    interval_seconds: Option<u32>,
    interval_days: Option<u32>,
    interval_months: Option<u32>,
    #[serde(default = "name_default")]
    name: String,
    restartable: bool,
    tts: bool,
    #[serde(default)]
    uid: String,
    username: Option<String>,
    utc_time: NaiveDateTime,
}

#[derive(Serialize, Deserialize, FromRow)]
pub struct ReminderCsv {
    attachment: Option<Attachment>,
    attachment_name: Option<String>,
    avatar: Option<String>,
    channel: String,
    content: String,
    embed_author: String,
    embed_author_url: Option<String>,
    embed_color: u32,
    embed_description: String,
    embed_footer: String,
    embed_footer_url: Option<String>,
    embed_image_url: Option<String>,
    embed_thumbnail_url: Option<String>,
    embed_title: String,
    embed_fields: Option<String>,
    enabled: bool,
    expires: Option<NaiveDateTime>,
    interval_seconds: Option<u32>,
    interval_days: Option<u32>,
    interval_months: Option<u32>,
    #[serde(default = "name_default")]
    name: String,
    restartable: bool,
    tts: bool,
    username: Option<String>,
    utc_time: NaiveDateTime,
}

#[derive(Deserialize)]
pub struct PatchReminder {
    uid: String,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    attachment: Unset<Option<Attachment>>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    attachment_name: Unset<Option<String>>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    avatar: Unset<Option<String>>,
    #[serde(default = "channel_default")]
    #[serde(with = "string")]
    channel: u64,
    #[serde(default)]
    content: Unset<String>,
    #[serde(default)]
    embed_author: Unset<String>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    embed_author_url: Unset<Option<String>>,
    #[serde(default)]
    embed_color: Unset<u32>,
    #[serde(default)]
    embed_description: Unset<String>,
    #[serde(default)]
    embed_footer: Unset<String>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    embed_footer_url: Unset<Option<String>>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    embed_image_url: Unset<Option<String>>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    embed_thumbnail_url: Unset<Option<String>>,
    #[serde(default)]
    embed_title: Unset<String>,
    #[serde(default)]
    embed_fields: Unset<Json<Vec<EmbedField>>>,
    #[serde(default)]
    enabled: Unset<bool>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    expires: Unset<Option<NaiveDateTime>>,
    #[serde(default = "interval_default")]
    #[serde(deserialize_with = "deserialize_optional_field")]
    interval_seconds: Unset<Option<u32>>,
    #[serde(default = "interval_default")]
    #[serde(deserialize_with = "deserialize_optional_field")]
    interval_days: Unset<Option<u32>>,
    #[serde(default = "interval_default")]
    #[serde(deserialize_with = "deserialize_optional_field")]
    interval_months: Unset<Option<u32>>,
    #[serde(default)]
    name: Unset<String>,
    #[serde(default)]
    restartable: Unset<bool>,
    #[serde(default)]
    tts: Unset<bool>,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_optional_field")]
    username: Unset<Option<String>>,
    #[serde(default)]
    utc_time: Unset<NaiveDateTime>,
}

impl PatchReminder {
    fn message_ok(&self) -> bool {
        self.content.as_ref().map_or(true, |c| c.len() <= MAX_CONTENT_LENGTH)
            && self.embed_author.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_AUTHOR_LENGTH)
            && self
                .embed_description
                .as_ref()
                .map_or(true, |c| c.len() <= MAX_EMBED_DESCRIPTION_LENGTH)
            && self.embed_footer.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_FOOTER_LENGTH)
            && self.embed_title.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_TITLE_LENGTH)
            && self.embed_fields.as_ref().map_or(true, |c| {
                c.0.len() <= MAX_EMBED_FIELDS
                    && c.0.iter().all(|f| {
                        f.title.len() <= MAX_EMBED_FIELD_TITLE_LENGTH
                            && f.value.len() <= MAX_EMBED_FIELD_VALUE_LENGTH
                    })
            })
            && self
                .username
                .as_ref()
                .map_or(true, |c| c.as_ref().map_or(true, |v| v.len() <= MAX_USERNAME_LENGTH))
    }
}

pub fn generate_uid() -> String {
    let mut generator: OsRng = Default::default();

    (0..64)
        .map(|_| CHARACTERS.chars().choose(&mut generator).unwrap().to_owned().to_string())
        .collect::<Vec<String>>()
        .join("")
}

fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    Ok(Some(Option::deserialize(deserializer)?))
}

#[derive(Deserialize)]
pub struct DeleteReminder {
    uid: String,
}

#[derive(Deserialize)]
pub struct ImportBody {
    body: String,
}

#[derive(Serialize, Deserialize)]
pub struct TodoCsv {
    value: String,
    channel_id: Option<String>,
}

pub(crate) async fn create_reminder(
    ctx: &Context,
    transaction: &mut Transaction<'_>,
    guild_id: GuildId,
    user_id: UserId,
    reminder: CreateReminder,
) -> JsonResult {
    // check guild in db
    match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.get())
        .fetch_one(transaction.executor())
        .await
    {
        Err(sqlx::Error::RowNotFound) => {
            if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.get())
                .execute(transaction.executor())
                .await
                .is_err()
            {
                return Err(json!({"error": "Guild could not be created"}));
            }
        }
        _ => {}
    }

    if !check_channel_matches_guild(ctx, ChannelId::new(reminder.channel), guild_id) {
        warn!(
            "Error in `create_reminder`: channel {} not found for guild {}",
            reminder.channel, guild_id
        );

        return Err(json!({"error": "Channel not found"}));
    }

    let channel =
        create_database_channel(&ctx, ChannelId::new(reminder.channel), transaction).await;

    if let Err(e) = channel {
        warn!("`create_database_channel` returned an error code: {:?}", e);

        // Provide more specific error messages based on the error type
        let error_msg = match e {
            Error::MissingDiscordPermission(permission) => {
                format!(
                    "Please ensure the bot has the \"{}\" permission in the channel",
                    permission
                )
            }
            _ => "Failed to configure channel for reminders. Please check the bot permissions"
                .to_string(),
        };

        return Err(json!({"error": error_msg}));
    }

    let channel = channel.unwrap();

    // validate lengths
    check_length!(MAX_NAME_LENGTH, reminder.name);
    check_length!(MAX_CONTENT_LENGTH, reminder.content);
    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description);
    check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title);
    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder.embed_author);
    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder.embed_footer);
    check_length_opt!(MAX_EMBED_FIELDS, reminder.embed_fields);
    if let Some(fields) = &reminder.embed_fields {
        for field in &fields.0 {
            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
        }
    }
    check_length_opt!(MAX_USERNAME_LENGTH, reminder.username);
    check_length_opt!(
        MAX_URL_LENGTH,
        reminder.embed_footer_url,
        reminder.embed_thumbnail_url,
        reminder.embed_author_url,
        reminder.embed_image_url,
        reminder.avatar
    );

    // validate urls
    check_url_opt!(
        reminder.embed_footer_url,
        reminder.embed_thumbnail_url,
        reminder.embed_author_url,
        reminder.embed_image_url,
        reminder.avatar
    );

    // validate time and interval
    if reminder.utc_time < Utc::now().naive_utc() {
        return Err(json!({"error": "Time must be in the future"}));
    }
    if reminder.interval_seconds.is_some()
        || reminder.interval_days.is_some()
        || reminder.interval_months.is_some()
    {
        if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32
            + reminder.interval_days.unwrap_or(0) * DAY as u32
            + reminder.interval_seconds.unwrap_or(0)
            < *MIN_INTERVAL
        {
            return Err(json!({"error": "Interval too short"}));
        }
    }

    // check patreon if necessary
    if reminder.interval_seconds.is_some()
        || reminder.interval_days.is_some()
        || reminder.interval_months.is_some()
    {
        if !check_subscription(&ctx, transaction.executor(), user_id, Some(guild_id)).await {
            return Err(json!({"error": "Patreon is required to set intervals"}));
        }
    }

    let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() };
    let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
        None
    } else {
        reminder.username
    };

    let new_uid = generate_uid();

    // write to db
    match sqlx::query!(
        "INSERT INTO reminders (
         uid,
         attachment,
         attachment_name,
         channel_id,
         avatar,
         content,
         embed_author,
         embed_author_url,
         embed_color,
         embed_description,
         embed_footer,
         embed_footer_url,
         embed_image_url,
         embed_thumbnail_url,
         embed_title,
         embed_fields,
         enabled,
         expires,
         interval_seconds,
         interval_days,
         interval_months,
         name,
         restartable,
         tts,
         username,
         `utc_time`
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        new_uid,
        reminder.attachment,
        reminder.attachment_name,
        channel,
        reminder.avatar,
        reminder.content,
        reminder.embed_author,
        reminder.embed_author_url,
        reminder.embed_color,
        reminder.embed_description,
        reminder.embed_footer,
        reminder.embed_footer_url,
        reminder.embed_image_url,
        reminder.embed_thumbnail_url,
        reminder.embed_title,
        reminder.embed_fields,
        reminder.enabled,
        reminder.expires,
        reminder.interval_seconds,
        reminder.interval_days,
        reminder.interval_months,
        name,
        reminder.restartable,
        reminder.tts,
        username,
        reminder.utc_time,
    )
    .execute(transaction.executor())
    .await
    {
        Ok(_) => sqlx::query_as_unchecked!(
            GetReminder,
            "
            SELECT
             reminders.attachment_name,
             reminders.avatar,
             channels.channel,
             reminders.content,
             reminders.embed_author,
             reminders.embed_author_url,
             reminders.embed_color,
             reminders.embed_description,
             reminders.embed_footer,
             reminders.embed_footer_url,
             reminders.embed_image_url,
             reminders.embed_thumbnail_url,
             reminders.embed_title,
             reminders.embed_fields,
             reminders.enabled,
             reminders.expires,
             reminders.interval_seconds,
             reminders.interval_days,
             reminders.interval_months,
             reminders.name,
             reminders.restartable,
             reminders.tts,
             reminders.uid,
             reminders.username,
             reminders.utc_time
            FROM reminders
            LEFT JOIN channels ON channels.id = reminders.channel_id
            WHERE uid = ?",
            new_uid
        )
        .fetch_one(transaction.executor())
        .await
        .map(|r| Ok(json!(r)))
        .unwrap_or_else(|e| {
            warn!("Failed to complete SQL query: {:?}", e);

            Err(json!({"error": "Could not load reminder"}))
        }),

        Err(e) => {
            warn!("Error in `create_reminder`: Could not execute query: {:?}", e);

            Err(json!({"error": "Unknown error"}))
        }
    }
}

pub(crate) async fn create_reminder_template(
    ctx: &Context,
    transaction: &mut Transaction<'_>,
    guild_id: GuildId,
    reminder_template: ReminderTemplate,
) -> JsonResult {
    check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
    check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
    check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
    if let Some(fields) = &reminder_template.embed_fields {
        for field in &fields.0 {
            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
        }
    }
    check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username);
    check_length_opt!(
        MAX_URL_LENGTH,
        reminder_template.embed_footer_url,
        reminder_template.embed_thumbnail_url,
        reminder_template.embed_author_url,
        reminder_template.embed_image_url,
        reminder_template.avatar
    );

    // validate urls
    check_url_opt!(
        reminder_template.embed_footer_url,
        reminder_template.embed_thumbnail_url,
        reminder_template.embed_author_url,
        reminder_template.embed_image_url,
        reminder_template.avatar
    );

    let name = if reminder_template.name.is_empty() {
        template_name_default()
    } else {
        reminder_template.name.clone()
    };

    match sqlx::query!(
        "INSERT INTO reminder_template
        (guild_id,
         name,
         attachment,
         attachment_name,
         avatar,
         content,
         embed_author,
         embed_author_url,
         embed_color,
         embed_description,
         embed_footer,
         embed_footer_url,
         embed_image_url,
         embed_thumbnail_url,
         embed_title,
         embed_fields,
         interval_seconds,
         interval_days,
         interval_months,
         tts,
         username
        ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
         ?, ?, ?, ?, ?, ?, ?)",
        guild_id.get(),
        name,
        reminder_template.attachment,
        reminder_template.attachment_name,
        reminder_template.avatar,
        reminder_template.content,
        reminder_template.embed_author,
        reminder_template.embed_author_url,
        reminder_template.embed_color,
        reminder_template.embed_description,
        reminder_template.embed_footer,
        reminder_template.embed_footer_url,
        reminder_template.embed_image_url,
        reminder_template.embed_thumbnail_url,
        reminder_template.embed_title,
        reminder_template.embed_fields,
        reminder_template.interval_seconds,
        reminder_template.interval_days,
        reminder_template.interval_months,
        reminder_template.tts,
        reminder_template.username,
    )
    .fetch_all(transaction.executor())
    .await
    {
        Ok(_) => Ok(json!({})),
        Err(e) => {
            warn!("Could not create template for {}: {:?}", guild_id.get(), e);

            json_err!("Could not create template")
        }
    }
}

fn check_channel_matches_guild(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> bool {
    match ctx.cache.guild(guild_id) {
        Some(guild) => guild.channels.get(&channel_id).is_some(),
        None => false,
    }
}

async fn create_database_channel(
    ctx: impl CacheHttp,
    channel: ChannelId,
    transaction: &mut Transaction<'_>,
) -> Result<u32, Error> {
    let row = sqlx::query!(
        "SELECT webhook_token, webhook_id FROM channels WHERE channel = ?",
        channel.get()
    )
    .fetch_one(transaction.executor())
    .await;

    match row {
        Ok(row) => {
            let is_dm = channel
                .to_channel(&ctx)
                .await
                .map_err(|e| {
                    if let serenity::Error::Http(http_error) = &e {
                        if let HttpError::UnsuccessfulRequest(response) = http_error {
                            if response.error.code == 50001 {
                                return Error::MissingDiscordPermission("View Channel");
                            }
                        }
                    }

                    Error::Serenity(e)
                })?
                .private()
                .is_some();
            if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
                let webhook = channel
                    .create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
                    .await
                    .map_err(|e| match &e {
                        serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
                            match response.error.code {
                                50001 => Error::MissingDiscordPermission("View Channel"),
                                50013 => Error::MissingDiscordPermission("Manage Webhooks"),
                                _ => Error::Serenity(e),
                            }
                        }
                        _ => Error::Serenity(e),
                    })?;

                let token = webhook.token.unwrap();

                sqlx::query!(
                    "
                    UPDATE channels SET webhook_id = ?, webhook_token = ? WHERE channel = ?
                    ",
                    webhook.id.get(),
                    token.expose_secret(),
                    channel.get()
                )
                .execute(transaction.executor())
                .await
                .map_err(|e| Error::SQLx(e))?;
            }

            Ok(())
        }

        Err(sqlx::Error::RowNotFound) => {
            // create webhook
            let webhook = channel
                .create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
                .await
                .map_err(|e| match &e {
                    serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
                        match response.error.code {
                            50001 => Error::MissingDiscordPermission("View Channel"),
                            50013 => Error::MissingDiscordPermission("Manage Webhooks"),
                            _ => Error::Serenity(e),
                        }
                    }
                    _ => Error::Serenity(e),
                })?;

            let token = webhook.token.unwrap();

            // create database entry
            sqlx::query!(
                "
                INSERT INTO channels (
                 webhook_id,
                 webhook_token,
                 channel
                ) VALUES (?, ?, ?)
                ",
                webhook.id.get(),
                token.expose_secret(),
                channel.get()
            )
            .execute(transaction.executor())
            .await
            .map_err(|e| Error::SQLx(e))?;

            Ok(())
        }

        Err(e) => Err(Error::SQLx(e)),
    }?;

    let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.get())
        .fetch_one(transaction.executor())
        .await
        .map_err(|e| Error::SQLx(e))?;

    Ok(row.id)
}

#[derive(Responder)]
pub enum DashboardPage {
    #[response(status = 200)]
    Ok(NamedFile),
    #[response(status = 302)]
    Unauthorised(Redirect),
    #[response(status = 500)]
    NotConfigured(Template),
}

// Legacy route to maintain compatibility with old dashboard routing
#[get("/?<id>")]
pub async fn reminders_redirect(id: &str) -> Redirect {
    Redirect::to(format!("/dashboard/{}/reminders", id))
}

// Legacy route to maintain compatibility with old dashboard routing
#[get("/todo?<id>")]
pub async fn todos_redirect(id: &str) -> Redirect {
    Redirect::to(format!("/dashboard/{}/todos", id))
}

#[get("/")]
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
    render_dashboard(cookies).await
}

#[get("/<_..>")]
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
    render_dashboard(cookies).await
}

async fn render_dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
    if cookies.get_private("userid").is_some() {
        match NamedFile::open(Path::new(path!("static/index.html"))).await {
            Ok(f) => DashboardPage::Ok(f),
            Err(e) => {
                warn!("Couldn't render dashboard: {:?}", e);

                DashboardPage::NotConfigured(internal_server_error().await)
            }
        }
    } else {
        DashboardPage::Unauthorised(Redirect::to("/login/discord"))
    }
}
