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

use crate::auth::oauth::FormData;
use chrono::Utc;
use common::{
    KV_RATE_LIMIT_CONTACT, Server,
    config::network::{ContactForm, FieldOrDefault},
    network::ip_to_bytes,
    psl,
};
use email::message::delivery::{IngestMessage, IngestRecipient, LocalDeliveryStatus, MailDelivery};
use http_proto::*;
use hyper::StatusCode;
use mail_auth::common::cache::NoCache;
use mail_builder::{
    MessageBuilder,
    headers::{
        HeaderType,
        address::{Address, EmailAddress},
    },
    mime::make_boundary,
};
use serde_json::json;
use std::{borrow::Cow, fmt::Write, future::Future};
use store::write::BatchBuilder;
use trc::AddContext;

pub trait FormHandler: Sync + Send {
    fn handle_contact_form(
        &self,
        session: &HttpSessionData,
        form: &ContactForm,
        form_data: FormData,
    ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}

impl FormHandler for Server {
    async fn handle_contact_form(
        &self,
        session: &HttpSessionData,
        form: &ContactForm,
        form_data: FormData,
    ) -> trc::Result<HttpResponse> {
        // Validate rate
        if let Some(rate) = &form.rate
            && !session.remote_ip.is_loopback()
            && self
                .in_memory_store()
                .is_rate_allowed(
                    KV_RATE_LIMIT_CONTACT,
                    &ip_to_bytes(&session.remote_ip),
                    rate,
                    false,
                )
                .await
                .caused_by(trc::location!())?
                .is_some()
        {
            return Err(trc::LimitEvent::TooManyRequests.into_err());
        }

        // Validate honeypot
        if form
            .field_honey_pot
            .as_ref()
            .is_some_and(|field| form_data.has_field(field))
        {
            return Err(trc::ResourceEvent::BadParameters
                .into_err()
                .details("Honey pot field present"));
        }

        // Obtain fields
        let from_email = form_data
            .get_or_default(&form.from_email)
            .trim()
            .to_lowercase();
        let from_subject = form_data.get_or_default(&form.from_subject).trim();
        let from_name = form_data.get_or_default(&form.from_name).trim();

        // Validate email
        let mut failure = None;
        let mut has_success = false;
        if form.validate_domain && from_email != form.from_email.default {
            if let Some(domain) = from_email.rsplit_once('@').and_then(|(local, domain)| {
                if !local.is_empty()
                    && domain.contains('.')
                    && psl::domain(domain.as_bytes()).is_some_and(|d| d.suffix().typ().is_some())
                {
                    Some(domain)
                } else {
                    None
                }
            }) {
                if self
                    .core
                    .smtp
                    .resolvers
                    .dns
                    .mx_lookup(domain, None::<&NoCache<_, _>>)
                    .await
                    .is_err()
                {
                    failure = Some(format!("No MX records found for domain {domain:?}. Please enter a valid email address.", ).into());
                }
            } else {
                failure = Some(Cow::Borrowed("Please enter a valid email address."));
            }
        }

        // Discard empty forms
        if failure.is_none() && form_data.fields().all(|(_, value)| value.trim().is_empty()) {
            failure = Some(Cow::Borrowed("Empty form"));
        }

        if failure.is_none() {
            // Build body
            let mut body = String::with_capacity(1024);
            for (field, value) in form_data.fields() {
                if !value.is_empty() {
                    body.push_str(field);
                    body.push_str(": ");
                    body.push_str(value);
                    body.push_str("\r\n");
                }
            }
            let _ = write!(
                &mut body,
                "Date: {}\r\n",
                Utc::now().format("%a, %d %b %Y %T %z")
            );
            let _ = write!(
                &mut body,
                "IP: {}:{}\r\n",
                session.remote_ip, session.remote_port
            );

            // Build message
            let message = MessageBuilder::new()
                .from((from_name, from_email.as_str()))
                .header(
                    "To",
                    HeaderType::Address(Address::List(
                        form.rcpt_to
                            .iter()
                            .map(|rcpt| {
                                Address::Address(EmailAddress {
                                    name: None,
                                    email: rcpt.into(),
                                })
                            })
                            .collect(),
                    )),
                )
                .header("Auto-Submitted", HeaderType::Text("auto-generated".into()))
                .message_id(format!(
                    "<{}@{}.{}>",
                    make_boundary("."),
                    session.remote_ip,
                    session.remote_port
                ))
                .subject(from_subject)
                .text_body(body)
                .write_to_vec()
                .unwrap_or_default();

            // Reserve and write blob
            let (message_blob, blob_hold) = self
                .put_temporary_blob(u32::MAX, &message, 60)
                .await
                .caused_by(trc::location!())?;

            for result in self
                .deliver_message(IngestMessage {
                    sender_address: from_email,
                    sender_authenticated: false,
                    recipients: form
                        .rcpt_to
                        .iter()
                        .map(|address| IngestRecipient {
                            address: address.clone(),
                            orcpt: None,
                            is_spam: false,
                        })
                        .collect(),
                    message_blob,
                    message_size: message.len() as u64,
                    session_id: session.session_id,
                })
                .await
                .status
            {
                match result {
                    LocalDeliveryStatus::Success => {
                        has_success = true;
                    }
                    LocalDeliveryStatus::TemporaryFailure { reason }
                    | LocalDeliveryStatus::PermanentFailure { reason, .. } => {
                        failure = Some(reason)
                    }
                }
            }

            // Remove blob hold
            let mut batch = BatchBuilder::new();
            batch.clear(blob_hold);
            self.store()
                .write(batch.build_all())
                .await
                .caused_by(trc::location!())?;

            // Suppress errors if there is at least one success
            if has_success {
                failure = None;
            }
        }

        Ok(JsonResponse::with_status(
            if has_success {
                StatusCode::OK
            } else {
                StatusCode::BAD_REQUEST
            },
            json!({
                "data": {
                    "success": has_success,
                    "details": failure,
                },
            }),
        )
        .into_http_response())
    }
}

impl FormData {
    pub fn get_or_default<'x>(&'x self, field: &'x FieldOrDefault) -> &'x str {
        if let Some(field_name) = &field.field {
            self.get(field_name)
                .filter(|f| !f.is_empty())
                .unwrap_or(field.default.as_str())
        } else {
            field.default.as_str()
        }
    }
}
