use anyhow::{Context as _, Result};
use diesel::prelude::*;
use tracing::Instrument as _;
use uuid::Uuid;

use crate::{
    app_state::AppState,
    channels::{Channel, EmailBackendConfig},
    email::MailSender,
    governors::Governor,
    messages::Message,
    schema::{channels, messages},
};

pub async fn run_worker(state: AppState) -> Result<()> {
    async move {
        process_messages(state.clone()).await?;
        reclaim_governor_entries(state).await?;
        Ok(())
    }
    .instrument(tracing::debug_span!("run_worker()"))
    .await
}

/**
 * Process messages from the queue in the `messages` table. Insertions to the
 * queue are rate limited per team and per project, so no effort should be
 * needed here to enforce fairness.
 */
async fn process_messages(state: AppState) -> Result<()> {
    async move {
        const MESSAGE_QUEUE_LIMIT: i64 = 250;
        let db_conn = state.db_pool.get().await?;
        let queued_messages = db_conn
            .interact::<_, Result<Vec<(Message, Channel)>>>(move |conn| {
                messages::table
                    .inner_join(channels::table)
                    .select((Message::as_select(), Channel::as_select()))
                    .filter(Message::is_not_sent())
                    .order(messages::created_at.asc())
                    .limit(MESSAGE_QUEUE_LIMIT)
                    .load(conn)
                    .context("failed to load queued messages")
            })
            .await
            .unwrap()?;
        // Dispatch email messages together to take advantage of Postmark's
        // batch send API
        let emails: Vec<(Uuid, crate::email::Message)> = queued_messages
            .iter()
            .filter_map(|(message, channel)| {
                if let Ok(backend_config) =
                    TryInto::<EmailBackendConfig>::try_into(channel.backend_config.clone())
                {
                    if backend_config.verified {
                        let recipient: lettre::message::Mailbox = if let Ok(recipient) =
                            backend_config.recipient.parse()
                        {
                            recipient
                        } else {
                            tracing::error!("failed to parse recipient for channel {}", channel.id);
                            return None;
                        };
                        let email = crate::email::Message {
                            from: state.settings.email.message_from.clone(),
                            to: recipient.into(),
                            subject: "Shout".to_string(),
                            text_body: message.message.clone(),
                        };
                        tracing::debug!("Sending email to recipient for channel {}", channel.id);
                        Some((message.id, email))
                    } else {
                        tracing::info!(
                            "Email recipient for channel {} is not verified",
                            channel.id
                        );
                        None
                    }
                } else {
                    None
                }
            })
            .collect();
        if !emails.is_empty() {
            let message_ids: Vec<Uuid> = emails.iter().map(|(id, _)| *id).collect();
            let results = state
                .mailer
                .send_batch(emails.into_iter().map(|(_, email)| email).collect())
                .await;
            assert!(results.len() == message_ids.len());
            let results_by_id = message_ids.into_iter().zip(results.into_iter());
            db_conn
                .interact::<_, Result<_>>(move |conn| {
                    for (id, result) in results_by_id {
                        if let Err(err) = result {
                            tracing::error!("error sending message {}: {:?}", id, err);
                        } else {
                            diesel::update(messages::table.filter(messages::id.eq(id)))
                                .set(messages::sent_at.eq(diesel::dsl::now))
                                .execute(conn)?;
                        }
                    }
                    Ok(())
                })
                .await
                .unwrap()?;
        }
        tracing::info!("finished processing messages");
        Ok(())
    }
    .instrument(tracing::debug_span!("process_messages()"))
    .await
}

async fn reclaim_governor_entries(state: AppState) -> Result<()> {
    async move {
        let db_conn = state.db_pool.get().await?;
        db_conn.interact(Governor::reclaim_all).await.unwrap()?;
        Ok(())
    }
    // This doesn't do much, since it seems that tracing spans don't carry
    // across into the thread spawned by db_conn.interact()
    .instrument(tracing::debug_span!("reclaim_governor_entries()"))
    .await
}
