/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use crate::{
    blob::download::BlobDownload, changes::state::JmapCacheState, email::ingested_into_object,
};
use common::{Server, auth::AccessToken};
use email::{
    cache::{MessageCacheFetch, mailbox::MailboxCacheAccess},
    mailbox::JUNK_ID,
    message::ingest::{EmailIngest, IngestEmail, IngestSource},
};
use http_proto::HttpSessionData;
use jmap_proto::{
    error::set::{SetError, SetErrorType},
    method::import::{ImportEmailRequest, ImportEmailResponse},
    object::email::EmailProperty,
    request::MaybeInvalid,
    types::state::State,
};
use mail_parser::{HeaderName, MessageParser};
use std::future::Future;
use types::{acl::Acl, id::Id, keyword::Keyword};
use utils::map::vec_map::VecMap;

pub trait EmailImport: Sync + Send {
    fn email_import(
        &self,
        request: ImportEmailRequest,
        access_token: &AccessToken,
        session: &HttpSessionData,
    ) -> impl Future<Output = trc::Result<ImportEmailResponse>> + Send;
}

impl EmailImport for Server {
    async fn email_import(
        &self,
        request: ImportEmailRequest,
        access_token: &AccessToken,
        session: &HttpSessionData,
    ) -> trc::Result<ImportEmailResponse> {
        // Validate state
        let account_id = request.account_id.document_id();
        let cache = self.get_cached_messages(account_id).await?;
        let old_state: State = cache.assert_state(false, &request.if_in_state)?;
        let can_add_mailbox_ids = if access_token.is_shared(account_id) {
            cache.shared_mailboxes(access_token, Acl::AddItems).into()
        } else {
            None
        };

        // Obtain import access token
        let import_access_token = if account_id != access_token.account_id() {
            #[cfg(feature = "test_mode")]
            {
                AccessToken::from_id_maybe_invalid(account_id).into()
            }

            #[cfg(not(feature = "test_mode"))]
            {
                use common::auth::BuildAccessToken;
                use trc::AddContext;
                self.access_token(account_id)
                    .await
                    .caused_by(trc::location!())?
                    .build()
                    .into()
            }
        } else {
            None
        };

        let mut response = ImportEmailResponse {
            account_id: request.account_id,
            new_state: old_state.clone(),
            old_state: old_state.into(),
            created: VecMap::with_capacity(request.emails.len()),
            not_created: VecMap::new(),
        };

        'outer: for (id, email) in request.emails {
            // Validate mailboxIds
            let mailbox_ids = email
                .mailbox_ids
                .unwrap()
                .into_iter()
                .filter_map(|m| m.try_unwrap().map(|m| m.document_id()))
                .collect::<Vec<_>>();
            if mailbox_ids.is_empty() {
                response.not_created.append(
                    id,
                    SetError::invalid_properties()
                        .with_property(EmailProperty::MailboxIds)
                        .with_description("Message must belong to at least one mailbox."),
                );
                continue;
            }
            for mailbox_id in &mailbox_ids {
                if !cache.has_mailbox_id(mailbox_id) {
                    response.not_created.append(
                        id,
                        SetError::invalid_properties()
                            .with_property(EmailProperty::MailboxIds)
                            .with_description(format!(
                                "Mailbox {} does not exist.",
                                Id::from(*mailbox_id)
                            )),
                    );
                    continue 'outer;
                } else if matches!(&can_add_mailbox_ids, Some(ids) if !ids.contains(*mailbox_id)) {
                    response.not_created.append(
                        id,
                        SetError::forbidden().with_description(format!(
                            "You are not allowed to add messages to mailbox {}.",
                            Id::from(*mailbox_id)
                        )),
                    );
                    continue 'outer;
                }
            }

            let MaybeInvalid::Value(blob_id) = email.blob_id else {
                response.not_created.append(
                    id,
                    SetError::invalid_properties()
                        .with_property(EmailProperty::BlobId)
                        .with_description("Invalid blob id."),
                );
                continue;
            };

            // Fetch raw message to import
            let raw_message = match self.blob_download(&blob_id, access_token).await? {
                Some(raw_message) => raw_message,
                None => {
                    response.not_created.append(
                        id,
                        SetError::new(SetErrorType::BlobNotFound)
                            .with_description(format!("BlobId {} not found.", blob_id)),
                    );
                    continue;
                }
            };

            // Import message
            let parsed = MessageParser::new().parse(&raw_message);
            let is_valid_message = parsed.as_ref().is_some_and(|message| {
                message
                    .headers()
                    .iter()
                    .any(|header| !matches!(header.name, HeaderName::Other(_)))
            });
            if !is_valid_message {
                response.not_created.append(
                    id,
                    SetError::new(SetErrorType::InvalidEmail)
                        .with_description("Blob does not contain a valid RFC 5322 message."),
                );
                continue;
            }
            match self
                .email_ingest(IngestEmail {
                    raw_message: &raw_message,
                    message: parsed,
                    blob_hash: Some(&blob_id.hash),
                    access_token: import_access_token.as_ref().unwrap_or(access_token),
                    source: IngestSource::Jmap {
                        train_classifier: email
                            .keywords
                            .iter()
                            .any(|k| matches!(k, Keyword::Junk | Keyword::NotJunk))
                            || mailbox_ids.contains(&JUNK_ID),
                    },
                    mailbox_ids,
                    keywords: email.keywords,
                    received_at: email.received_at.map(|r| r.into()),
                    session_id: session.session_id,
                })
                .await
            {
                Ok(email) => {
                    response
                        .created
                        .append(id, ingested_into_object(email).into());
                }
                Err(mut err) => match err.as_ref() {
                    trc::EventType::Limit(trc::LimitEvent::Quota) => {
                        response.not_created.append(
                            id,
                            SetError::new(SetErrorType::OverQuota)
                                .with_description("You have exceeded your disk quota."),
                        );
                    }
                    trc::EventType::MessageIngest(trc::MessageIngestEvent::Error) => {
                        response.not_created.append(
                            id,
                            SetError::new(SetErrorType::InvalidEmail).with_description(
                                err.take_value(trc::Key::Reason)
                                    .and_then(|v| v.into_string())
                                    .unwrap(),
                            ),
                        );
                    }
                    _ => {
                        return Err(err);
                    }
                },
            }
        }

        // Update state
        if !response.created.is_empty() {
            response.new_state = self.get_cached_messages(account_id).await?.get_state(false);
        }

        Ok(response)
    }
}
