use async_graphql::{
    connection::{self, Connection, Edge, OpaqueCursor},
    Context, EmptySubscription, Enum, Error, FieldResult, InputObject, Object, Schema,
    SimpleObject, Union,
};
use log::info;
use notmuch::Notmuch;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;

use crate::{config::Config, newsreader, nm, Query};

/// # Number of seconds since the Epoch
pub type UnixTime = isize;

/// # Thread ID, sans "thread:"
pub type ThreadId = String;

#[derive(Debug, SimpleObject)]
pub struct ThreadSummary {
    pub thread: ThreadId,
    pub timestamp: UnixTime,
    /// user-friendly timestamp
    pub date_relative: String,
    /// number of matched messages
    pub matched: isize,
    /// total messages in thread
    pub total: isize,
    /// comma-separated names with | between matched and unmatched
    pub authors: String,
    pub subject: String,
    pub tags: Vec<String>,
}

#[derive(Debug, Union)]
pub enum Thread {
    Email(EmailThread),
    News(NewsPost),
}

#[derive(Debug, SimpleObject)]
pub struct NewsPost {
    pub thread_id: String,
    pub is_read: bool,
    pub slug: String,
    pub site: String,
    pub title: String,
    pub body: String,
    pub url: String,
    pub timestamp: i64,
}

#[derive(Debug, SimpleObject)]
pub struct EmailThread {
    pub thread_id: String,
    pub subject: String,
    pub messages: Vec<Message>,
}

#[derive(Debug, SimpleObject)]
pub struct Message {
    // Message-ID for message, prepend `id:<id>` to search in notmuch
    pub id: String,
    // First From header found in email
    pub from: Option<Email>,
    // All To headers found in email
    pub to: Vec<Email>,
    // All CC headers found in email
    pub cc: Vec<Email>,
    // First Subject header found in email
    pub subject: Option<String>,
    // Parsed Date header, if found and valid
    pub timestamp: Option<i64>,
    // Headers
    pub headers: Vec<Header>,
    // The body contents
    pub body: Body,
    // On disk location of message
    pub path: String,
    pub attachments: Vec<Attachment>,
    pub tags: Vec<String>,
}

// Content-Type: image/jpeg; name="PXL_20231125_204826860.jpg"
// Content-Disposition: attachment; filename="PXL_20231125_204826860.jpg"
// Content-Transfer-Encoding: base64
// Content-ID: <f_lponoluo1>
// X-Attachment-Id: f_lponoluo1
#[derive(Default, Debug, SimpleObject)]
pub struct Attachment {
    pub id: String,
    pub idx: String,
    pub filename: Option<String>,
    pub size: usize,
    pub content_type: Option<String>,
    pub content_id: Option<String>,
    pub disposition: DispositionType,
    pub bytes: Vec<u8>,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Disposition {
    pub r#type: DispositionType,
    pub filename: Option<String>,
    pub size: Option<usize>,
}

#[derive(Debug, Enum, Copy, Clone, Eq, PartialEq)]
pub enum DispositionType {
    Inline,
    Attachment,
}

impl From<mailparse::DispositionType> for DispositionType {
    fn from(value: mailparse::DispositionType) -> Self {
        match value {
            mailparse::DispositionType::Inline => DispositionType::Inline,
            mailparse::DispositionType::Attachment => DispositionType::Attachment,
            dt => panic!("unhandled DispositionType {dt:?}"),
        }
    }
}

impl Default for DispositionType {
    fn default() -> Self {
        DispositionType::Attachment
    }
}

#[derive(Debug, SimpleObject)]
pub struct Header {
    pub key: String,
    pub value: String,
}

#[derive(Debug)]
pub struct UnhandledContentType {
    pub text: String,
    pub content_tree: String,
}

#[Object]
impl UnhandledContentType {
    async fn contents(&self) -> &str {
        &self.text
    }
    async fn content_tree(&self) -> &str {
        &self.content_tree
    }
}

#[derive(Debug)]
pub struct PlainText {
    pub text: String,
    pub content_tree: String,
}

#[Object]
impl PlainText {
    async fn contents(&self) -> &str {
        &self.text
    }
    async fn content_tree(&self) -> &str {
        &self.content_tree
    }
}

#[derive(Debug)]
pub struct Html {
    pub html: String,
    pub content_tree: String,
}

#[Object]
impl Html {
    async fn contents(&self) -> &str {
        &self.html
    }
    async fn content_tree(&self) -> &str {
        &self.content_tree
    }
    async fn headers(&self) -> Vec<Header> {
        Vec::new()
    }
}

#[derive(Debug, Union)]
pub enum Body {
    UnhandledContentType(UnhandledContentType),
    PlainText(PlainText),
    Html(Html),
}

impl Body {
    pub fn html(html: String) -> Body {
        Body::Html(Html {
            html,
            content_tree: "".to_string(),
        })
    }
    pub fn text(text: String) -> Body {
        Body::PlainText(PlainText {
            text,
            content_tree: "".to_string(),
        })
    }
}

#[derive(Debug, SimpleObject)]
pub struct Email {
    pub name: Option<String>,
    pub addr: Option<String>,
}

#[derive(SimpleObject)]
pub struct Tag {
    pub name: String,
    pub fg_color: String,
    pub bg_color: String,
    pub unread: usize,
}

#[derive(Serialize, Deserialize, Debug, InputObject)]
struct SearchCursor {
    newsreader_offset: i32,
    notmuch_offset: i32,
}

pub struct QueryRoot;
#[Object]
impl QueryRoot {
    async fn version<'ctx>(&self, _ctx: &Context<'ctx>) -> Result<String, Error> {
        build_info::build_info!(fn bi);
        Ok(shared::build_version(bi))
    }
    async fn count<'ctx>(&self, ctx: &Context<'ctx>, query: String) -> Result<usize, Error> {
        let nm = ctx.data_unchecked::<Notmuch>();
        let pool = ctx.data_unchecked::<PgPool>();

        let newsreader_query: Query = query.parse()?;

        let newsreader_count = newsreader::count(pool, &newsreader_query).await?;
        let notmuch_count = nm::count(nm, &newsreader_query.to_notmuch()).await?;
        info!("count {newsreader_query:?} newsreader count {newsreader_count} notmuch count {notmuch_count}");
        Ok(newsreader_count + notmuch_count)
    }

    async fn search<'ctx>(
        &self,
        ctx: &Context<'ctx>,
        after: Option<String>,
        before: Option<String>,
        first: Option<i32>,
        last: Option<i32>,
        query: String,
    ) -> Result<Connection<OpaqueCursor<SearchCursor>, ThreadSummary>, Error> {
        // TODO: add keywords to limit search to one corpus, i.e. is:news or is:mail
        info!("search({after:?} {before:?} {first:?} {last:?} {query:?})",);
        let nm = ctx.data_unchecked::<Notmuch>();
        let pool = ctx.data_unchecked::<PgPool>();

        enum ThreadSummaryCursor {
            Newsreader(i32, ThreadSummary),
            Notmuch(i32, ThreadSummary),
        }
        Ok(connection::query(
            after,
            before,
            first,
            last,
            |after: Option<OpaqueCursor<SearchCursor>>,
             before: Option<OpaqueCursor<SearchCursor>>,
             first: Option<usize>,
             last: Option<usize>| async move {
                info!(
                    "search({:?} {:?} {first:?} {last:?} {query:?})",
                    after.as_ref().map(|v| &v.0),
                    before.as_ref().map(|v| &v.0)
                );
                let newsreader_after = after.as_ref().map(|sc| sc.newsreader_offset);
                let notmuch_after = after.as_ref().map(|sc| sc.notmuch_offset);
                let newsreader_before = before.as_ref().map(|sc| sc.newsreader_offset);
                let notmuch_before = before.as_ref().map(|sc| sc.notmuch_offset);

                let newsreader_query: Query = query.parse()?;
                info!("newsreader_query {newsreader_query:?}");
                let newsreader_results = if newsreader_query.is_newsreader {
                    newsreader::search(
                        pool,
                        newsreader_after,
                        newsreader_before,
                        first.map(|v| v as i32),
                        last.map(|v| v as i32),
                        &newsreader_query,
                    )
                    .await?
                    .into_iter()
                    .map(|(cur, ts)| ThreadSummaryCursor::Newsreader(cur, ts))
                    .collect()
                } else {
                    Vec::new()
                };

                let notmuch_results = if newsreader_query.is_notmuch {
                    nm::search(
                        nm,
                        notmuch_after,
                        notmuch_before,
                        first.map(|v| v as i32),
                        last.map(|v| v as i32),
                        newsreader_query.to_notmuch(),
                    )
                    .await?
                    .into_iter()
                    .map(|(cur, ts)| ThreadSummaryCursor::Notmuch(cur, ts))
                    .collect()
                } else {
                    Vec::new()
                };

                let mut results: Vec<_> = newsreader_results
                    .into_iter()
                    .chain(notmuch_results)
                    .collect();

                // The leading '-' is to reverse sort
                results.sort_by_key(|item| match item {
                    ThreadSummaryCursor::Newsreader(_, ts) => -ts.timestamp,
                    ThreadSummaryCursor::Notmuch(_, ts) => -ts.timestamp,
                });

                let mut has_next_page = before.is_some();
                if let Some(first) = first {
                    if results.len() > first {
                        has_next_page = true;
                        results.truncate(first);
                    }
                }

                let mut has_previous_page = after.is_some();
                if let Some(last) = last {
                    if results.len() > last {
                        has_previous_page = true;
                        results.truncate(last);
                    }
                }

                let mut connection = Connection::new(has_previous_page, has_next_page);
                let mut newsreader_offset = 0;
                let mut notmuch_offset = 0;

                connection.edges.extend(results.into_iter().map(|item| {
                    let thread_summary;
                    match item {
                        ThreadSummaryCursor::Newsreader(offset, ts) => {
                            thread_summary = ts;
                            newsreader_offset = offset;
                        }
                        ThreadSummaryCursor::Notmuch(offset, ts) => {
                            thread_summary = ts;
                            notmuch_offset = offset;
                        }
                    }
                    let cur = OpaqueCursor(SearchCursor {
                        newsreader_offset,
                        notmuch_offset,
                    });
                    Edge::new(cur, thread_summary)
                }));
                Ok::<_, async_graphql::Error>(connection)
            },
        )
        .await?)
    }

    async fn tags<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Vec<Tag>> {
        let nm = ctx.data_unchecked::<Notmuch>();
        let pool = ctx.data_unchecked::<PgPool>();
        let needs_unread = ctx.look_ahead().field("unread").exists();
        let mut tags = newsreader::tags(pool, needs_unread).await?;
        tags.append(&mut nm::tags(nm, needs_unread)?);
        Ok(tags)
    }
    async fn thread<'ctx>(&self, ctx: &Context<'ctx>, thread_id: String) -> Result<Thread, Error> {
        let nm = ctx.data_unchecked::<Notmuch>();
        let pool = ctx.data_unchecked::<PgPool>();
        let config = ctx.data_unchecked::<Config>();
        let debug_content_tree = ctx
            .look_ahead()
            .field("messages")
            .field("body")
            .field("contentTree")
            .exists();
        // TODO: look at thread_id and conditionally load newsreader
        if newsreader::is_newsreader_thread(&thread_id) {
            Ok(newsreader::thread(config, pool, thread_id).await?)
        } else {
            Ok(nm::thread(nm, thread_id, debug_content_tree).await?)
        }
    }
}

pub struct Mutation;
#[Object]
impl Mutation {
    async fn set_read_status<'ctx>(
        &self,
        ctx: &Context<'ctx>,
        query: String,
        unread: bool,
    ) -> Result<bool, Error> {
        let nm = ctx.data_unchecked::<Notmuch>();
        let pool = ctx.data_unchecked::<PgPool>();

        for q in query.split_whitespace() {
            if newsreader::is_newsreader_thread(&q) {
                newsreader::set_read_status(pool, &q, unread).await?;
            } else {
                nm::set_read_status(nm, q, unread).await?;
            }
        }
        Ok(true)
    }
    async fn tag_add<'ctx>(
        &self,
        ctx: &Context<'ctx>,
        query: String,
        tag: String,
    ) -> Result<bool, Error> {
        let nm = ctx.data_unchecked::<Notmuch>();
        info!("tag_add({tag}, {query})");
        nm.tag_add(&tag, &query)?;
        Ok(true)
    }
    async fn tag_remove<'ctx>(
        &self,
        ctx: &Context<'ctx>,
        query: String,
        tag: String,
    ) -> Result<bool, Error> {
        let nm = ctx.data_unchecked::<Notmuch>();
        info!("tag_remove({tag}, {query})");
        nm.tag_remove(&tag, &query)?;
        Ok(true)
    }
}

pub type GraphqlSchema = Schema<QueryRoot, Mutation, EmptySubscription>;
