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

use super::get::VacationResponseGet;
use crate::changes::state::StateManager;
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use email::sieve::{SieveScript, VacationResponse, ingest::SieveScriptIngest};
use jmap_proto::{
    error::set::{SetError, SetErrorType},
    method::set::{SetRequest, SetResponse},
    object::vacation_response::{self, VacationResponseProperty, VacationResponseValue},
    references::resolve::ResolveCreatedReference,
    request::IntoValid,
    types::date::UTCDate,
};
use jmap_tools::{Key, Map, Value};
use mail_builder::MessageBuilder;
use mail_parser::decoders::html::html_to_text;
use std::borrow::Cow;
use std::future::Future;
use store::{
    Serialize, SerializeInfallible, ValueKey,
    write::{AlignedBytes, Archive, Archiver, BatchBuilder},
};
use trc::AddContext;
use types::{
    collection::{Collection, SyncCollection},
    field::PrincipalField,
    id::Id,
};

pub trait VacationResponseSet: Sync + Send {
    fn vacation_response_set(
        &self,
        request: SetRequest<'_, vacation_response::VacationResponse>,
        access_token: &AccessToken,
    ) -> impl Future<Output = trc::Result<SetResponse<vacation_response::VacationResponse>>> + Send;

    fn build_script(&self, obj: &mut SieveScript) -> trc::Result<Vec<u8>>;
}

impl VacationResponseSet for Server {
    async fn vacation_response_set(
        &self,
        mut request: SetRequest<'_, vacation_response::VacationResponse>,
        access_token: &AccessToken,
    ) -> trc::Result<SetResponse<vacation_response::VacationResponse>> {
        let account_id = request.account_id.document_id();
        let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
            .with_state(
                self.assert_state(
                    account_id,
                    SyncCollection::SieveScript,
                    &request.if_in_state,
                )
                .await?,
            );

        for id in request.unwrap_destroy().into_valid() {
            response.not_destroyed.append(id, SetError::singleton());
        }

        // Process set or update requests
        let create_id = None;
        let mut changes = None;
        match (request.create, request.update) {
            (Some(create), Some(update)) if !create.is_empty() && !update.is_empty() => {
                return Err(trc::JmapEvent::InvalidArguments
                    .into_err()
                    .details("Creating and updating on the same request is not allowed."));
            }
            (Some(create), _) if !create.is_empty() => {
                for (id, _obj) in create {
                    response.not_created.append(id, SetError::singleton());
                }
            }
            (_, Some(update)) if !update.is_empty() => {
                for (id, obj) in update.into_valid() {
                    if id.is_singleton() {
                        changes = Some(obj);
                    } else {
                        response.not_updated.append(
                            id,
                            SetError::new(SetErrorType::NotFound).with_description("ID not found."),
                        );
                    }
                }
            }
            _ => {
                return Ok(response);
            }
        }

        // Prepare write batch
        let mut batch = BatchBuilder::new();
        batch
            .with_account_id(account_id)
            .with_collection(Collection::SieveScript);

        // Process changes
        let active_script_id = self.sieve_script_get_active_id(account_id).await?;
        if let Some(changes) = changes {
            // Obtain current script
            let document_id = self.get_vacation_sieve_script_id(account_id).await?;

            let (mut sieve, prev_sieve) = if let Some(document_id) = document_id {
                let prev_sieve = self
                    .store()
                    .get_value::<Archive<AlignedBytes>>(ValueKey::archive(
                        account_id,
                        Collection::SieveScript,
                        document_id,
                    ))
                    .await?
                    .ok_or_else(|| {
                        trc::StoreEvent::NotFound
                            .into_err()
                            .caused_by(trc::location!())
                    })?
                    .into_deserialized::<SieveScript>()
                    .caused_by(trc::location!())?;
                let mut sieve = prev_sieve.inner.clone();
                if sieve.vacation_response.is_none() {
                    sieve.vacation_response = VacationResponse::default().into();
                }

                (sieve, Some(prev_sieve))
            } else {
                (
                    SieveScript {
                        name: "vacation".into(),
                        blob_hash: Default::default(),
                        size: 0,
                        vacation_response: VacationResponse::default().into(),
                    },
                    None,
                )
            };

            // Parse properties
            let mut is_active = false;
            let mut build_script = create_id.is_some();
            let vacation = sieve.vacation_response.as_mut().unwrap();

            for (property, mut value) in changes.into_expanded_object() {
                if let Err(err) = response.resolve_self_references(&mut value, 0, false) {
                    return Ok(set_error(response, create_id, err));
                };

                match (&property, value) {
                    (Key::Property(VacationResponseProperty::Subject), Value::Str(value))
                        if value.len() < 512 =>
                    {
                        build_script = true;
                        vacation.subject = Some(value.into_owned());
                    }
                    (Key::Property(VacationResponseProperty::HtmlBody), Value::Str(value))
                        if value.len() < 2048 =>
                    {
                        build_script = true;
                        vacation.html_body = Some(value.into_owned());
                    }
                    (Key::Property(VacationResponseProperty::TextBody), Value::Str(value))
                        if value.len() < 2048 =>
                    {
                        build_script = true;
                        vacation.text_body = Some(value.into_owned());
                    }
                    (
                        Key::Property(VacationResponseProperty::FromDate),
                        Value::Element(VacationResponseValue::Date(date)),
                    ) => {
                        vacation.from_date = Some(date.timestamp() as u64);
                        build_script = true;
                    }
                    (
                        Key::Property(VacationResponseProperty::ToDate),
                        Value::Element(VacationResponseValue::Date(date)),
                    ) => {
                        vacation.to_date = Some(date.timestamp() as u64);
                        build_script = true;
                    }
                    (Key::Property(VacationResponseProperty::IsEnabled), Value::Bool(value)) => {
                        is_active = value;
                    }
                    (Key::Property(VacationResponseProperty::IsEnabled), Value::Null) => {
                        is_active = false;
                    }
                    (
                        Key::Property(
                            VacationResponseProperty::Subject
                            | VacationResponseProperty::HtmlBody
                            | VacationResponseProperty::TextBody
                            | VacationResponseProperty::ToDate
                            | VacationResponseProperty::FromDate,
                        ),
                        Value::Null,
                    ) => {
                        if create_id.is_none() {
                            build_script = true;
                            match property {
                                Key::Property(VacationResponseProperty::Subject) => {
                                    vacation.subject = None;
                                }
                                Key::Property(VacationResponseProperty::HtmlBody) => {
                                    vacation.html_body = None;
                                }
                                Key::Property(VacationResponseProperty::TextBody) => {
                                    vacation.text_body = None;
                                }
                                Key::Property(VacationResponseProperty::FromDate) => {
                                    vacation.from_date = None;
                                }
                                Key::Property(VacationResponseProperty::ToDate) => {
                                    vacation.to_date = None;
                                }
                                _ => unreachable!(),
                            }
                        }
                    }
                    (Key::Property(VacationResponseProperty::Id), value) => {
                        if create_id.is_some() || !crate::matches_id(&value, Id::singleton()) {
                            return Ok(set_error(
                                response,
                                create_id,
                                SetError::invalid_properties()
                                    .with_property(VacationResponseProperty::Id)
                                    .with_description("The id property is immutable."),
                            ));
                        }
                    }
                    _ => {
                        return Ok(set_error(
                            response,
                            create_id,
                            SetError::invalid_properties()
                                .with_property(property.into_owned())
                                .with_description("Field could not be set."),
                        ));
                    }
                }
            }

            let mut obj = ObjectIndexBuilder::new()
                .with_current_opt(prev_sieve)
                .with_changes(sieve)
                .with_changed_by(access_token.account_tenant_ids());

            // Update id
            let document_id = if let Some(document_id) = document_id {
                batch.with_document(document_id);
                document_id
            } else {
                let document_id = self
                    .store()
                    .assign_document_ids(account_id, Collection::SieveScript, 1)
                    .await
                    .caused_by(trc::location!())?;
                batch.with_document(document_id);
                document_id
            };

            // Create sieve script only if there are changes
            if build_script {
                // Upload new blob
                let (blob_hash, blob_hold) = self
                    .put_temporary_blob(
                        account_id,
                        &self.build_script(obj.changes_mut().unwrap())?,
                        60,
                    )
                    .await?;
                obj.changes_mut().unwrap().blob_hash = blob_hash;
                batch.clear(blob_hold);
            };
            batch.custom(obj).caused_by(trc::location!())?;

            // Deactivate other sieve scripts
            let was_active = active_script_id == Some(document_id);
            if is_active {
                if !was_active {
                    batch
                        .with_collection(Collection::Principal)
                        .with_document(0)
                        .set(PrincipalField::ActiveScriptId, document_id.serialize());
                }
            } else if was_active {
                batch
                    .with_collection(Collection::Principal)
                    .with_document(0)
                    .clear(PrincipalField::ActiveScriptId);
            }

            // Write changes
            if !batch.is_empty() {
                response.new_state = Some(
                    self.commit_batch(batch)
                        .await
                        .and_then(|ids| ids.last_change_id(account_id))
                        .caused_by(trc::location!())?
                        .into(),
                );
            }

            // Add result
            if let Some(create_id) = create_id {
                response.created.insert(
                    create_id,
                    Map::with_capacity(1)
                        .with_key_value(VacationResponseProperty::Id, Id::singleton())
                        .into(),
                );
            } else {
                response.updated.append(Id::singleton(), None);
            }
        }

        Ok(response)
    }

    fn build_script(&self, obj: &mut SieveScript) -> trc::Result<Vec<u8>> {
        // Build Sieve script
        let mut script = Vec::with_capacity(1024);
        script.extend_from_slice(b"require [\"vacation\", \"relational\", \"date\"];\r\n\r\n");
        let mut num_blocks = 0;

        // Add start date
        if let Some(value) = obj.vacation_response.as_ref().and_then(|v| v.from_date) {
            script.extend_from_slice(b"if currentdate :value \"ge\" \"iso8601\" \"");
            script.extend_from_slice(UTCDate::from(value).to_string().as_bytes());
            script.extend_from_slice(b"\" {\r\n");
            num_blocks += 1;
        }

        // Add end date
        if let Some(value) = obj.vacation_response.as_ref().and_then(|v| v.to_date) {
            script.extend_from_slice(b"if currentdate :value \"le\" \"iso8601\" \"");
            script.extend_from_slice(UTCDate::from(value).to_string().as_bytes());
            script.extend_from_slice(b"\" {\r\n");
            num_blocks += 1;
        }

        script.extend_from_slice(b"vacation :mime ");
        if let Some(value) = obj
            .vacation_response
            .as_ref()
            .and_then(|v| v.subject.as_ref())
        {
            script.extend_from_slice(b":subject \"");
            for &ch in value.as_bytes().iter() {
                match ch {
                    b'\\' | b'\"' => {
                        script.push(b'\\');
                    }
                    b'\r' | b'\n' => {
                        continue;
                    }
                    _ => (),
                }
                script.push(ch);
            }
            script.extend_from_slice(b"\" ");
        }

        let mut text_body = if let Some(value) = obj
            .vacation_response
            .as_ref()
            .and_then(|v| v.text_body.as_ref())
        {
            Cow::from(value.as_str()).into()
        } else {
            None
        };
        let html_body = if let Some(value) = obj
            .vacation_response
            .as_ref()
            .and_then(|v| v.html_body.as_ref())
        {
            Cow::from(value.as_str()).into()
        } else {
            None
        };
        match (&html_body, &text_body) {
            (Some(html_body), None) => {
                text_body = Cow::from(html_to_text(html_body.as_ref())).into();
            }
            (None, None) => {
                text_body = Cow::from("I am away.").into();
            }
            _ => (),
        }

        let mut builder = MessageBuilder::new();
        let mut body_len = 0;
        if let Some(html_body) = html_body {
            body_len = html_body.len();
            builder = builder.html_body(html_body);
        }
        if let Some(text_body) = text_body {
            body_len += text_body.len();
            builder = builder.text_body(text_body);
        }
        let mut message_body = Vec::with_capacity(body_len + 128);
        builder.write_body(&mut message_body).ok();

        script.push(b'\"');
        for ch in message_body {
            if [b'\\', b'\"'].contains(&ch) {
                script.push(b'\\');
            }
            script.push(ch);
        }
        script.extend_from_slice(b"\";\r\n");

        // Close blocks
        for _ in 0..num_blocks {
            script.extend_from_slice(b"}\r\n");
        }

        match self.core.sieve.untrusted_compiler.compile(&script) {
            Ok(compiled_script) => {
                // Update blob length
                obj.size = script.len() as u32;

                // Serialize script
                script.extend(
                    Archiver::new(compiled_script)
                        .untrusted()
                        .serialize()
                        .caused_by(trc::location!())?,
                );

                Ok(script)
            }
            Err(err) => Err(trc::StoreEvent::UnexpectedError
                .caused_by(trc::location!())
                .reason(err)
                .details("Vacation Sieve Script failed to compile.")),
        }
    }
}

fn set_error(
    mut response: SetResponse<vacation_response::VacationResponse>,
    id: Option<String>,
    err: SetError<VacationResponseProperty>,
) -> SetResponse<vacation_response::VacationResponse> {
    if let Some(id) = id {
        response.not_created.append(id, err);
    } else {
        response.not_updated.append(Id::singleton(), err);
    }
    response
}
