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

use super::ingest::{EmailIngest, IngestEmail, IngestSource};
use crate::{mailbox::INBOX_ID, sieve::ingest::SieveScriptIngest};
use common::Server;
use directory::Permission;
use mail_parser::MessageParser;
use std::{borrow::Cow, future::Future};
use store::ahash::AHashMap;
use types::{
    blob_hash::BlobHash,
    type_state::{DataType, StateChange},
};

#[derive(Debug)]
pub struct IngestMessage {
    pub sender_address: String,
    pub sender_authenticated: bool,
    pub recipients: Vec<String>,
    pub message_blob: BlobHash,
    pub message_size: u64,
    pub session_id: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LocalDeliveryStatus {
    Success,
    TemporaryFailure {
        reason: Cow<'static, str>,
    },
    PermanentFailure {
        code: [u8; 3],
        reason: Cow<'static, str>,
    },
}

pub struct LocalDeliveryResult {
    pub status: Vec<LocalDeliveryStatus>,
    pub autogenerated: Vec<AutogeneratedMessage>,
}

pub struct AutogeneratedMessage {
    pub sender_address: String,
    pub recipients: Vec<String>,
    pub message: Vec<u8>,
}

pub trait MailDelivery: Sync + Send {
    fn deliver_message(
        &self,
        message: IngestMessage,
    ) -> impl Future<Output = LocalDeliveryResult> + Send;
}

impl MailDelivery for Server {
    async fn deliver_message(&self, message: IngestMessage) -> LocalDeliveryResult {
        // Read message
        let raw_message = match self
            .core
            .storage
            .blob
            .get_blob(message.message_blob.as_slice(), 0..usize::MAX)
            .await
        {
            Ok(Some(raw_message)) => raw_message,
            Ok(None) => {
                trc::event!(
                    MessageIngest(trc::MessageIngestEvent::Error),
                    Reason = "Blob not found.",
                    SpanId = message.session_id,
                    CausedBy = trc::location!()
                );

                return LocalDeliveryResult {
                    status: (0..message.recipients.len())
                        .map(|_| LocalDeliveryStatus::TemporaryFailure {
                            reason: "Blob not found.".into(),
                        })
                        .collect::<Vec<_>>(),
                    autogenerated: vec![],
                };
            }
            Err(err) => {
                trc::error!(
                    err.details("Failed to fetch message blob.")
                        .span_id(message.session_id)
                        .caused_by(trc::location!())
                );

                return LocalDeliveryResult {
                    status: (0..message.recipients.len())
                        .map(|_| LocalDeliveryStatus::TemporaryFailure {
                            reason: "Temporary I/O error.".into(),
                        })
                        .collect::<Vec<_>>(),
                    autogenerated: vec![],
                };
            }
        };

        // Obtain the UIDs for each recipient
        let mut uids: AHashMap<u32, usize> = AHashMap::with_capacity(message.recipients.len());
        let mut result = LocalDeliveryResult {
            status: Vec::with_capacity(message.recipients.len()),
            autogenerated: Vec::new(),
        };

        for rcpt in message.recipients {
            let uid = match self
                .email_to_id(&self.core.storage.directory, &rcpt, message.session_id)
                .await
            {
                Ok(Some(uid)) => uid,
                Ok(None) => {
                    // Something went wrong
                    result.status.push(LocalDeliveryStatus::PermanentFailure {
                        code: [5, 5, 0],
                        reason: "Mailbox not found.".into(),
                    });
                    continue;
                }
                Err(err) => {
                    trc::error!(
                        err.details("Failed to lookup recipient.")
                            .ctx(trc::Key::To, rcpt)
                            .span_id(message.session_id)
                            .caused_by(trc::location!())
                    );
                    result.status.push(LocalDeliveryStatus::TemporaryFailure {
                        reason: "Address lookup failed.".into(),
                    });
                    continue;
                }
            };
            if let Some(status) = uids.get(&uid).and_then(|pos| result.status.get(*pos)) {
                result.status.push(status.clone());
                continue;
            }

            // Obtain access token
            let status = match self.get_access_token(uid).await.and_then(|token| {
                token
                    .assert_has_permission(Permission::EmailReceive)
                    .map(|_| token)
            }) {
                Ok(access_token) => {
                    // Check if there is an active sieve script
                    match self.sieve_script_get_active(uid).await {
                        Ok(None) => {
                            // Ingest message
                            self.email_ingest(IngestEmail {
                                raw_message: &raw_message,
                                message: MessageParser::new().parse(&raw_message),
                                access_token: &access_token,
                                mailbox_ids: vec![INBOX_ID],
                                keywords: vec![],
                                received_at: None,
                                source: IngestSource::Smtp {
                                    deliver_to: &rcpt,
                                    is_sender_authenticated: message.sender_authenticated,
                                },
                                spam_classify: access_token
                                    .has_permission(Permission::SpamFilterClassify),
                                spam_train: self.email_bayes_can_train(&access_token),
                                session_id: message.session_id,
                            })
                            .await
                        }
                        Ok(Some(active_script)) => {
                            self.sieve_script_ingest(
                                &access_token,
                                &raw_message,
                                &message.sender_address,
                                message.sender_authenticated,
                                &rcpt,
                                message.session_id,
                                active_script,
                                &mut result.autogenerated,
                            )
                            .await
                        }
                        Err(err) => Err(err),
                    }
                }

                Err(err) => Err(err),
            };

            let status = match status {
                Ok(ingested_message) => {
                    // Notify state change
                    if ingested_message.change_id != u64::MAX {
                        self.broadcast_state_change(
                            StateChange::new(uid, ingested_message.change_id)
                                .with_change(DataType::EmailDelivery)
                                .with_change(DataType::Email)
                                .with_change(DataType::Mailbox)
                                .with_change(DataType::Thread),
                        )
                        .await;
                    }

                    LocalDeliveryStatus::Success
                }
                Err(err) => {
                    let status = match err.as_ref() {
                        trc::EventType::Limit(trc::LimitEvent::Quota) => {
                            LocalDeliveryStatus::TemporaryFailure {
                                reason: "Mailbox over quota.".into(),
                            }
                        }
                        trc::EventType::Limit(trc::LimitEvent::TenantQuota) => {
                            LocalDeliveryStatus::TemporaryFailure {
                                reason: "Organization over quota.".into(),
                            }
                        }
                        trc::EventType::Security(trc::SecurityEvent::Unauthorized) => {
                            LocalDeliveryStatus::PermanentFailure {
                                code: [5, 5, 0],
                                reason: "This account is not authorized to receive email.".into(),
                            }
                        }
                        trc::EventType::MessageIngest(trc::MessageIngestEvent::Error) => {
                            LocalDeliveryStatus::PermanentFailure {
                                code: err
                                    .value(trc::Key::Code)
                                    .and_then(|v| v.to_uint())
                                    .map(|n| {
                                        [(n / 100) as u8, ((n % 100) / 10) as u8, (n % 10) as u8]
                                    })
                                    .unwrap_or([5, 5, 0]),
                                reason: err
                                    .value_as_str(trc::Key::Reason)
                                    .unwrap_or_default()
                                    .to_string()
                                    .into(),
                            }
                        }
                        _ => LocalDeliveryStatus::TemporaryFailure {
                            reason: "Transient server failure.".into(),
                        },
                    };

                    trc::error!(
                        err.ctx(trc::Key::To, rcpt.to_string())
                            .span_id(message.session_id)
                    );

                    status
                }
            };

            // Cache response for UID to avoid duplicate deliveries
            uids.insert(uid, result.status.len());

            result.status.push(status);
        }

        result
    }
}
