#[macro_use]
extern crate rocket;
use std::{error::Error, io::Cursor, str::FromStr};

use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema};
use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse};
use glog::Flags;
use notmuch::{Notmuch, NotmuchError, ThreadSet};
use rocket::{
    http::{ContentType, Header},
    request::Request,
    response::{content, Debug, Responder},
    serde::json::Json,
    Response, State,
};
use rocket_cors::{AllowedHeaders, AllowedOrigins};
use server::{
    error::ServerError,
    graphql::{
        attachment_bytes, cid_attachment_bytes, Attachment, GraphqlSchema, Mutation, QueryRoot,
    },
};

#[get("/refresh")]
async fn refresh(nm: &State<Notmuch>) -> Result<Json<String>, Debug<NotmuchError>> {
    Ok(Json(String::from_utf8_lossy(&nm.new()?).to_string()))
}

#[get("/search")]
async fn search_all(
    nm: &State<Notmuch>,
) -> Result<Json<shared::SearchResult>, Debug<NotmuchError>> {
    search(nm, "*", None, None).await
}

#[get("/search/<query>?<page>&<results_per_page>")]
async fn search(
    nm: &State<Notmuch>,
    query: &str,
    page: Option<usize>,
    results_per_page: Option<usize>,
) -> Result<Json<shared::SearchResult>, Debug<NotmuchError>> {
    let page = page.unwrap_or(0);
    let results_per_page = results_per_page.unwrap_or(20);
    let query = urlencoding::decode(query).map_err(NotmuchError::from)?;
    info!(" search '{query}'");
    let res = shared::SearchResult {
        summary: nm.search(&query, page * results_per_page, results_per_page)?,
        query: query.to_string(),
        page,
        results_per_page,
        total: nm.count(&query)?,
    };
    Ok(Json(res))
}

#[get("/show/<query>/pretty")]
async fn show_pretty(
    nm: &State<Notmuch>,
    query: &str,
) -> Result<Json<ThreadSet>, Debug<ServerError>> {
    let query = urlencoding::decode(query).map_err(|e| ServerError::from(NotmuchError::from(e)))?;
    let res = nm.show(&query).map_err(ServerError::from)?;
    Ok(Json(res))
}

#[get("/show/<query>")]
async fn show(nm: &State<Notmuch>, query: &str) -> Result<Json<ThreadSet>, Debug<NotmuchError>> {
    let query = urlencoding::decode(query).map_err(NotmuchError::from)?;
    let res = nm.show(&query)?;
    Ok(Json(res))
}

struct InlineAttachmentResponder(Attachment);

impl<'r, 'o: 'r> Responder<'r, 'o> for InlineAttachmentResponder {
    fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> {
        let mut resp = Response::build();
        if let Some(filename) = self.0.filename {
            resp.header(Header::new(
                "Content-Disposition",
                format!(r#"inline; filename="{}""#, filename),
            ));
        }
        if let Some(content_type) = self.0.content_type {
            if let Some(ct) = ContentType::parse_flexible(&content_type) {
                resp.header(ct);
            }
        }
        resp.sized_body(self.0.bytes.len(), Cursor::new(self.0.bytes))
            .ok()
    }
}

struct DownloadAttachmentResponder(Attachment);

impl<'r, 'o: 'r> Responder<'r, 'o> for DownloadAttachmentResponder {
    fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> {
        let mut resp = Response::build();
        if let Some(filename) = self.0.filename {
            resp.header(Header::new(
                "Content-Disposition",
                format!(r#"attachment; filename="{}""#, filename),
            ));
        }
        if let Some(content_type) = self.0.content_type {
            if let Some(ct) = ContentType::parse_flexible(&content_type) {
                resp.header(ct);
            }
        }
        resp.sized_body(self.0.bytes.len(), Cursor::new(self.0.bytes))
            .ok()
    }
}

#[get("/cid/<id>/<cid>")]
async fn view_cid(
    nm: &State<Notmuch>,
    id: &str,
    cid: &str,
) -> Result<InlineAttachmentResponder, Debug<ServerError>> {
    let mid = if id.starts_with("id:") {
        id.to_string()
    } else {
        format!("id:{}", id)
    };
    info!("view cid attachment {mid} {cid}");
    let attachment = cid_attachment_bytes(nm, &mid, &cid)?;
    Ok(InlineAttachmentResponder(attachment))
}

#[get("/view/attachment/<id>/<idx>/<_>")]
async fn view_attachment(
    nm: &State<Notmuch>,
    id: &str,
    idx: &str,
) -> Result<InlineAttachmentResponder, Debug<ServerError>> {
    let mid = if id.starts_with("id:") {
        id.to_string()
    } else {
        format!("id:{}", id)
    };
    info!("view attachment {mid} {idx}");
    let idx: Vec<_> = idx
        .split('.')
        .map(|s| s.parse().expect("not a usize"))
        .collect();
    let attachment = attachment_bytes(nm, &mid, &idx)?;
    Ok(InlineAttachmentResponder(attachment))
}

#[get("/download/attachment/<id>/<idx>/<_>")]
async fn download_attachment(
    nm: &State<Notmuch>,
    id: &str,
    idx: &str,
) -> Result<DownloadAttachmentResponder, Debug<ServerError>> {
    let mid = if id.starts_with("id:") {
        id.to_string()
    } else {
        format!("id:{}", id)
    };
    info!("download attachment {mid} {idx}");
    let idx: Vec<_> = idx
        .split('.')
        .map(|s| s.parse().expect("not a usize"))
        .collect();
    let attachment = attachment_bytes(nm, &mid, &idx)?;
    Ok(DownloadAttachmentResponder(attachment))
}

#[get("/original/<id>")]
async fn original(
    nm: &State<Notmuch>,
    id: &str,
) -> Result<(ContentType, Vec<u8>), Debug<NotmuchError>> {
    let mid = if id.starts_with("id:") {
        id.to_string()
    } else {
        format!("id:{}", id)
    };
    let res = nm.show_original(&mid)?;
    Ok((ContentType::Plain, res))
}

#[rocket::get("/")]
fn graphiql() -> content::RawHtml<String> {
    content::RawHtml(GraphiQLSource::build().endpoint("/graphql").finish())
}

#[rocket::get("/graphql?<query..>")]
async fn graphql_query(schema: &State<GraphqlSchema>, query: GraphQLQuery) -> GraphQLResponse {
    query.execute(schema.inner()).await
}

#[rocket::post("/graphql", data = "<request>", format = "application/json")]
async fn graphql_request(
    schema: &State<GraphqlSchema>,
    request: GraphQLRequest,
) -> GraphQLResponse {
    request.execute(schema.inner()).await
}

#[rocket::main]
async fn main() -> Result<(), Box<dyn Error>> {
    glog::new()
        .init(Flags {
            colorlogtostderr: true,
            //alsologtostderr: true, // use logtostderr to only write to stderr and not to files
            logtostderr: true,
            ..Default::default()
        })
        .unwrap();
    let allowed_origins = AllowedOrigins::all();
    let cors = rocket_cors::CorsOptions {
        allowed_origins,
        allowed_methods: vec!["Get"]
            .into_iter()
            .map(|s| FromStr::from_str(s).unwrap())
            .collect(),
        allowed_headers: AllowedHeaders::some(&["Authorization", "Accept"]),
        allow_credentials: true,
        ..Default::default()
    }
    .to_cors()?;

    let schema = Schema::build(QueryRoot, Mutation, EmptySubscription)
        .data(Notmuch::default())
        .extension(async_graphql::extensions::Logger)
        .finish();

    let _ = rocket::build()
        .mount(
            shared::urls::MOUNT_POINT,
            routes![
                original,
                refresh,
                search_all,
                search,
                show_pretty,
                show,
                graphql_query,
                graphql_request,
                graphiql,
                view_cid,
                view_attachment,
                download_attachment,
            ],
        )
        .attach(cors)
        .manage(schema)
        .manage(Notmuch::default())
        //.manage(Notmuch::with_config("../notmuch/testdata/notmuch.config"))
        .launch()
        .await?;

    Ok(())
}
