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

use crate::{
    error::set::SetError,
    method::{
        copy::CopyRequest,
        get::GetRequest,
        import::ImportEmailRequest,
        parse::ParseRequest,
        search_snippet::GetSearchSnippetRequest,
        set::{SetRequest, SetResponse},
        upload::{BlobUploadRequest, DataSourceObject},
    },
    object::{AnyId, JmapObject, JmapObjectId},
    references::{Graph, eval::EvalObjectReferences, topological_sort},
    request::{
        CopyRequestMethod, GetRequestMethod, MaybeInvalid, ParseRequestMethod, RequestMethod,
        SetRequestMethod,
        reference::{MaybeIdReference, MaybeResultReference},
    },
    response::Response,
};
use compact_str::format_compact;
use jmap_tools::{Element, Key, Property, Value};
use std::collections::HashMap;
use types::id::Id;

impl Response<'_> {
    pub fn resolve_references(&self, request: &mut RequestMethod) -> trc::Result<()> {
        match request {
            RequestMethod::Get(request) => match request {
                GetRequestMethod::Email(request) => request.resolve_references(self)?,
                GetRequestMethod::Mailbox(request) => request.resolve_references(self)?,
                GetRequestMethod::Thread(request) => request.resolve_references(self)?,
                GetRequestMethod::Identity(request) => request.resolve_references(self)?,
                GetRequestMethod::EmailSubmission(request) => request.resolve_references(self)?,
                GetRequestMethod::PushSubscription(request) => request.resolve_references(self)?,
                GetRequestMethod::Sieve(request) => request.resolve_references(self)?,
                GetRequestMethod::VacationResponse(request) => request.resolve_references(self)?,
                GetRequestMethod::Principal(request) => request.resolve_references(self)?,
                GetRequestMethod::Quota(request) => request.resolve_references(self)?,
                GetRequestMethod::Blob(request) => request.resolve_references(self)?,
                GetRequestMethod::AddressBook(request) => request.resolve_references(self)?,
                GetRequestMethod::ContactCard(request) => request.resolve_references(self)?,
                GetRequestMethod::FileNode(request) => request.resolve_references(self)?,
                GetRequestMethod::ShareNotification(request) => request.resolve_references(self)?,
                GetRequestMethod::Calendar(request) => request.resolve_references(self)?,
                GetRequestMethod::CalendarEvent(request) => request.resolve_references(self)?,
                GetRequestMethod::CalendarEventNotification(request) => {
                    request.resolve_references(self)?
                }
                GetRequestMethod::ParticipantIdentity(request) => {
                    request.resolve_references(self)?
                }
                GetRequestMethod::PrincipalAvailability(_) => (),
                GetRequestMethod::Registry(request) => request.resolve_references(self)?,
            },
            RequestMethod::Set(request) => match request {
                SetRequestMethod::Email(request) => request.resolve_references(self, 2, false)?,
                SetRequestMethod::Mailbox(request) => request.resolve_references(self, 1, false)?,
                SetRequestMethod::Identity(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::EmailSubmission(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::PushSubscription(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::Sieve(request) => request.resolve_references(self, 1, false)?,
                SetRequestMethod::VacationResponse(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::AddressBook(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::ContactCard(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::FileNode(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::ShareNotification(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::Calendar(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::CalendarEvent(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::CalendarEventNotification(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::ParticipantIdentity(request) => {
                    request.resolve_references(self, 1, false)?
                }
                SetRequestMethod::Registry(request) => request.resolve_references(self, 5, true)?,
            },
            RequestMethod::Copy(request) => match request {
                CopyRequestMethod::Email(request) => request.resolve_references(self, 1, false)?,
                CopyRequestMethod::CalendarEvent(request) => {
                    request.resolve_references(self, 1, false)?
                }
                CopyRequestMethod::ContactCard(request) => {
                    request.resolve_references(self, 1, false)?
                }
                CopyRequestMethod::FileNode(request) => {
                    request.resolve_references(self, 1, false)?
                }
                CopyRequestMethod::Blob(_) => (),
            },
            RequestMethod::ImportEmail(request) => request.resolve_references(self)?,
            RequestMethod::SearchSnippet(request) => request.resolve_references(self)?,
            RequestMethod::UploadBlob(request) => request.resolve_references(self)?,
            RequestMethod::Parse(request) => match request {
                ParseRequestMethod::Email(request) => request.resolve_references(self)?,
                ParseRequestMethod::ContactCard(request) => request.resolve_references(self)?,
                ParseRequestMethod::CalendarEvent(request) => request.resolve_references(self)?,
            },
            _ => {}
        }

        Ok(())
    }
}

pub trait ResolveCreatedReference<P, E>
where
    P: Property,
    E: Element<Property = P> + JmapObjectId,
{
    fn get_created_id(&self, id_ref: &str) -> Option<AnyId>;

    fn resolve_self_references(
        &self,
        value: &mut Value<'_, P, E>,
        depth: usize,
        eval_strings: bool,
    ) -> Result<(), SetError<P>> {
        match value {
            Value::Object(obj) if eval_strings && depth < 5 => {
                for (key, value) in obj.as_mut_vec() {
                    if let Some(id) = key
                        .as_string_key()
                        .and_then(|k| k.strip_prefix('#'))
                        .and_then(|id_ref| self.get_created_id(id_ref))
                    {
                        *key = Key::Owned(match id {
                            AnyId::Id(id) => id.to_string(),
                            AnyId::BlobId(id) => id.to_string(),
                        });
                    }

                    if matches!(value, Value::Object(_) | Value::Array(_)) {
                        self.resolve_self_references(value, depth + 1, eval_strings)?;
                    }
                }
            }
            Value::Element(element) => {
                if let Some(id_ref) = element.as_id_ref() {
                    if let Some(id) = self.get_created_id(id_ref) {
                        if !element.try_set_id(id) {
                            return Err(SetError::invalid_properties()
                                .with_description("Id reference points to invalid type."));
                        }
                    } else {
                        return Err(SetError::not_found()
                            .with_description(format!("Id reference {id_ref:?} not found.")));
                    }
                }
            }
            Value::Array(items) if depth < 5 => {
                for item in items {
                    self.resolve_self_references(item, depth + 1, eval_strings)?;
                }
            }
            _ => {}
        }

        Ok(())
    }
}

pub(crate) trait ResolveReference {
    fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()>;
}

pub(crate) trait ResolveSetReference {
    fn resolve_references(
        &mut self,
        response: &Response<'_>,
        max_depth: usize,
        eval_strings: bool,
    ) -> trc::Result<()>;
}

impl<T: JmapObject> ResolveReference for GetRequest<T> {
    fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
        // Resolve id references
        match &mut self.ids {
            Some(MaybeResultReference::Reference(reference)) => {
                self.ids = Some(MaybeResultReference::Value(
                    response
                        .eval_result_references(reference)?
                        .into_ids::<T::Id>(reference)
                        .map(|f| f.map(MaybeIdReference::Id))
                        .collect::<Result<_, _>>()?,
                ));
            }
            Some(MaybeResultReference::Value(ids)) => {
                for id in ids {
                    if let MaybeIdReference::Reference(reference) = id {
                        if let Some(resolved_id) = response
                            .created_ids
                            .get(reference)
                            .cloned()
                            .and_then(|v| T::Id::try_from(v).ok())
                        {
                            *id = MaybeIdReference::Id(resolved_id);
                        } else {
                            return Err(trc::JmapEvent::InvalidResultReference.into_err().details(
                                format_compact!(
                                    "Id reference {reference:?} does not exist or is invalid."
                                ),
                            ));
                        }
                    }
                }
            }
            _ => (),
        }

        // Resolve properties references
        if let Some(MaybeResultReference::Reference(reference)) = &self.properties {
            self.properties = Some(MaybeResultReference::Value(
                response
                    .eval_result_references(reference)?
                    .into_properties::<T::Property>(reference)
                    .map(|f| f.map(MaybeInvalid::Value))
                    .collect::<Result<_, _>>()?,
            ));
        }

        Ok(())
    }
}

impl<'x, T: JmapObject> ResolveSetReference for SetRequest<'x, T> {
    fn resolve_references(
        &mut self,
        response: &Response<'_>,
        max_depth: usize,
        eval_strings: bool,
    ) -> trc::Result<()> {
        // Resolve create references
        if let Some(create) = &mut self.create {
            let mut graph = HashMap::with_capacity(create.len());
            for (id, obj) in create.iter_mut() {
                obj.eval_object_references(
                    response,
                    &mut Graph::Some {
                        child_id: &*id,
                        graph: &mut graph,
                    },
                    0,
                    max_depth,
                    eval_strings,
                )?;
            }

            // Perform topological sort
            if !graph.is_empty() {
                self.create = topological_sort(create, graph)?.into();
            }
        }

        // Resolve update references
        if let Some(update) = &mut self.update {
            for obj in update.values_mut() {
                obj.eval_object_references(response, &mut Graph::None, 0, max_depth, eval_strings)?;
            }
        }

        // Resolve destroy references
        if let Some(MaybeResultReference::Reference(reference)) = &self.destroy {
            self.destroy = Some(MaybeResultReference::Value(
                response
                    .eval_result_references(reference)?
                    .into_ids::<Id>(reference)
                    .map(|f| f.map(MaybeInvalid::Value))
                    .collect::<Result<_, _>>()?,
            ));
        }

        Ok(())
    }
}

impl<'x, T: JmapObject> ResolveSetReference for CopyRequest<'x, T> {
    fn resolve_references(
        &mut self,
        response: &Response<'_>,
        max_depth: usize,
        eval_strings: bool,
    ) -> trc::Result<()> {
        // Resolve create references
        for (id, obj) in self.create.iter_mut() {
            obj.eval_object_references(response, &mut Graph::None, 0, max_depth, eval_strings)?;

            if let MaybeIdReference::Reference(ir) = id {
                *id = MaybeIdReference::Id(response.eval_id_reference(ir)?);
            }
        }

        Ok(())
    }
}

impl<T: JmapObject> ResolveReference for ParseRequest<T> {
    fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
        // Resolve blobId references
        for id in self.blob_ids.iter_mut() {
            if let MaybeIdReference::Reference(ir) = id {
                *id = MaybeIdReference::Id(response.eval_blob_id_reference(ir)?);
            }
        }

        Ok(())
    }
}

impl ResolveReference for ImportEmailRequest {
    fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
        // Resolve email mailbox references
        for email in self.emails.values_mut() {
            match &mut email.mailbox_ids {
                MaybeResultReference::Reference(reference) => {
                    email.mailbox_ids = MaybeResultReference::Value(
                        response
                            .eval_result_references(reference)?
                            .into_ids::<Id>(reference)
                            .map(|f| f.map(MaybeIdReference::Id))
                            .collect::<Result<_, _>>()?,
                    );
                }
                MaybeResultReference::Value(values) => {
                    for value in values {
                        if let MaybeIdReference::Reference(ir) = value {
                            *value = MaybeIdReference::Id(response.eval_id_reference(ir)?);
                        }
                    }
                }
            }
        }

        Ok(())
    }
}

impl ResolveReference for GetSearchSnippetRequest {
    fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
        // Resolve emailIds references
        if let MaybeResultReference::Reference(reference) = &self.email_ids {
            self.email_ids = MaybeResultReference::Value(
                response
                    .eval_result_references(reference)?
                    .into_ids::<Id>(reference)
                    .map(|f| f.map(MaybeInvalid::Value))
                    .collect::<Result<_, _>>()?,
            );
        }

        Ok(())
    }
}

impl ResolveReference for BlobUploadRequest {
    fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
        let mut graph = HashMap::with_capacity(self.create.len());
        for (create_id, object) in self.create.iter_mut() {
            for data in &mut object.data {
                if let DataSourceObject::Id { id, .. } = data
                    && let MaybeIdReference::Reference(parent_id) = id
                {
                    match response.created_ids.get(parent_id) {
                        Some(AnyId::BlobId(blob_id)) => {
                            *id = MaybeIdReference::Id(blob_id.clone());
                        }
                        Some(_) => {
                            return Err(trc::JmapEvent::InvalidResultReference.into_err().details(
                                format_compact!(
                                    "Id reference {parent_id:?} points to invalid type."
                                ),
                            ));
                        }
                        None => {
                            graph
                                .entry(create_id.to_string())
                                .or_insert_with(Vec::new)
                                .push(parent_id.to_string());
                        }
                    }
                }
            }
        }

        // Perform topological sort
        if !graph.is_empty() {
            self.create = topological_sort(&mut self.create, graph)?;
        }

        Ok(())
    }
}

impl<T> ResolveCreatedReference<T::Property, T::Element> for SetResponse<T>
where
    T: JmapObject,
{
    fn get_created_id(&self, id_ref: &str) -> Option<AnyId> {
        self.created
            .get(id_ref)
            .and_then(|v| v.as_object())
            .and_then(|v| v.get(&Key::Property(T::ID_PROPERTY)))
            .and_then(|v| v.as_element())
            .and_then(|v| v.as_any_id())
    }
}
