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

use crate::{
    core::{MailboxId, Session, SessionData, State},
    op::ImapContext,
    spawn_op,
};
use common::{
    auth::AccessToken, ipc::CacheInvalidation, network::SessionStream, sharing::EffectiveAcl,
    storage::index::ObjectIndexBuilder,
};
use compact_str::ToCompactString;
use imap_proto::{
    Command, ResponseCode, StatusResponse,
    protocol::acl::{
        Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
    },
    receiver::Request,
};
use registry::schema::enums::Permission;
use std::time::Instant;
use store::{
    ValueKey,
    write::{AlignedBytes, Archive, BatchBuilder},
};
use trc::AddContext;
use types::{
    acl::{Acl, AclGrant},
    collection::Collection,
};
use utils::map::bitmap::Bitmap;

impl<T: SessionStream> Session<T> {
    pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
        // Validate access
        self.assert_has_permission(Permission::ImapAclGet)?;

        let op_start = Instant::now();
        let arguments = request.parse_acl(self.is_utf8)?;
        let is_utf8 = self.version.is_rev2() || self.is_utf8;
        let data = self.state.session_data();

        spawn_op!(data, {
            let (mailbox_id, mailbox_, _) = data
                .get_acl_mailbox(&arguments, true)
                .await
                .imap_ctx(&arguments.tag, trc::location!())?;
            let mut permissions = Vec::new();
            let mailbox = mailbox_
                .to_unarchived::<email::mailbox::Mailbox>()
                .imap_ctx(&arguments.tag, trc::location!())?;

            // Add the current user if they are the owner or a group member
            if data.access_token.is_member(mailbox_id.account_id) {
                let account_name = data
                    .server
                    .account(mailbox_id.account_id)
                    .await
                    .imap_ctx(&arguments.tag, trc::location!())?
                    .name()
                    .to_string();

                permissions.push((
                    account_name,
                    vec![
                        Rights::Read,
                        Rights::Lookup,
                        Rights::Insert,
                        Rights::DeleteMessages,
                        Rights::Expunge,
                        Rights::Seen,
                        Rights::Write,
                        Rights::CreateMailbox,
                        Rights::DeleteMailbox,
                        Rights::Post,
                        Rights::Administer,
                    ],
                ));
            }

            for item in mailbox.inner.acls.iter() {
                if item.account_id == mailbox_id.account_id {
                    // Skip the current user, as they are already added above
                    continue;
                }

                let mut rights = Vec::new();

                for acl in Bitmap::from(&item.grants) {
                    match acl {
                        Acl::Read => {
                            rights.push(Rights::Lookup);
                        }
                        Acl::Modify => {
                            rights.push(Rights::CreateMailbox);
                        }
                        Acl::Delete => {
                            rights.push(Rights::DeleteMailbox);
                        }
                        Acl::ReadItems => {
                            rights.push(Rights::Read);
                        }
                        Acl::AddItems => {
                            rights.push(Rights::Insert);
                        }
                        Acl::ModifyItems => {
                            rights.push(Rights::Write);
                            rights.push(Rights::Seen);
                        }
                        Acl::RemoveItems => {
                            rights.push(Rights::DeleteMessages);
                            rights.push(Rights::Expunge);
                        }
                        Acl::CreateChild => {
                            rights.push(Rights::CreateMailbox);
                        }
                        Acl::Share => {
                            rights.push(Rights::Administer);
                        }
                        Acl::Submit => {
                            rights.push(Rights::Post);
                        }
                        _ => (),
                    }
                }

                let account_name = data
                    .server
                    .account(item.account_id.into())
                    .await
                    .imap_ctx(&arguments.tag, trc::location!())?
                    .name()
                    .to_string();

                permissions.push((account_name, rights));
            }

            trc::event!(
                Imap(trc::ImapEvent::GetAcl),
                SpanId = data.session_id,
                MailboxName = arguments.mailbox_name.clone(),
                AccountId = mailbox_id.account_id,
                MailboxId = mailbox_id.mailbox_id,
                Total = permissions.len(),
                Elapsed = op_start.elapsed()
            );

            data.write_bytes(
                StatusResponse::completed(Command::GetAcl)
                    .with_tag(arguments.tag)
                    .serialize(
                        GetAclResponse {
                            mailbox_name: arguments.mailbox_name.to_string(),
                            permissions,
                        }
                        .into_bytes(is_utf8),
                    ),
            )
            .await
        })
    }

    pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
        // Validate access
        self.assert_has_permission(Permission::ImapMyRights)?;

        let op_start = Instant::now();
        let arguments = request.parse_acl(self.is_utf8)?;
        let data = self.state.session_data();
        let is_utf8 = self.version.is_rev2() || self.is_utf8;

        spawn_op!(data, {
            let (mailbox_id, mailbox_, access_token) = data
                .get_acl_mailbox(&arguments, false)
                .await
                .imap_ctx(&arguments.tag, trc::location!())?;
            let mailbox = mailbox_
                .to_unarchived::<email::mailbox::Mailbox>()
                .imap_ctx(&arguments.tag, trc::location!())?;
            let rights = if access_token.is_shared(mailbox_id.account_id) {
                let acl = mailbox.inner.acls.effective_acl(&access_token);
                let mut rights = Vec::with_capacity(5);
                if acl.contains(Acl::ReadItems) {
                    rights.push(Rights::Read);
                    rights.push(Rights::Lookup);
                }
                if acl.contains(Acl::AddItems) {
                    rights.push(Rights::Insert);
                }
                if acl.contains(Acl::RemoveItems) {
                    rights.push(Rights::DeleteMessages);
                    rights.push(Rights::Expunge);
                }
                if acl.contains(Acl::ModifyItems) {
                    rights.push(Rights::Seen);
                    rights.push(Rights::Write);
                }
                if acl.contains(Acl::CreateChild) {
                    rights.push(Rights::CreateMailbox);
                }
                if acl.contains(Acl::Delete) {
                    rights.push(Rights::DeleteMailbox);
                }
                if acl.contains(Acl::Submit) {
                    rights.push(Rights::Post);
                }
                rights
            } else {
                vec![
                    Rights::Read,
                    Rights::Lookup,
                    Rights::Insert,
                    Rights::DeleteMessages,
                    Rights::Expunge,
                    Rights::Seen,
                    Rights::Write,
                    Rights::CreateMailbox,
                    Rights::DeleteMailbox,
                    Rights::Post,
                    Rights::Administer,
                ]
            };

            trc::event!(
                Imap(trc::ImapEvent::MyRights),
                SpanId = data.session_id,
                MailboxName = arguments.mailbox_name.clone(),
                AccountId = mailbox_id.account_id,
                MailboxId = mailbox_id.mailbox_id,
                Details = rights
                    .iter()
                    .map(|r| trc::Value::String(r.to_compact_string()))
                    .collect::<Vec<_>>(),
                Elapsed = op_start.elapsed()
            );

            data.write_bytes(
                StatusResponse::completed(Command::MyRights)
                    .with_tag(arguments.tag)
                    .serialize(
                        MyRightsResponse {
                            mailbox_name: arguments.mailbox_name.to_string(),
                            rights,
                        }
                        .into_bytes(is_utf8),
                    ),
            )
            .await
        })
    }

    pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
        // Validate access
        self.assert_has_permission(Permission::ImapAclSet)?;

        let op_start = Instant::now();
        let command = request.command;
        let arguments = request.parse_acl(self.is_utf8)?;
        let data = self.state.session_data();

        spawn_op!(data, {
            // Validate mailbox
            let (mailbox_id, current_mailbox, _) = data
                .get_acl_mailbox(&arguments, true)
                .await
                .imap_ctx(&arguments.tag, trc::location!())?;
            let current_mailbox = current_mailbox
                .into_deserialized::<email::mailbox::Mailbox>()
                .imap_ctx(&arguments.tag, trc::location!())?;

            // Obtain principal id
            let acl_account_id = data
                .server
                .account_id_from_email(arguments.identifier.as_ref().unwrap(), false)
                .await
                .imap_ctx(&arguments.tag, trc::location!())?
                .ok_or_else(|| {
                    trc::ImapEvent::Error
                        .into_err()
                        .details("Account does not exist")
                        .id(arguments.tag.to_string())
                        .caused_by(trc::location!())
                })?;

            // Prepare changes
            let mut mailbox = current_mailbox.inner.clone();
            let (op, rights) = arguments
                .mod_rights
                .map(|mr| {
                    (
                        mr.op,
                        Bitmap::from_iter(mr.rights.into_iter().map(Acl::from)),
                    )
                })
                .unwrap_or_else(|| (ModRightsOp::Replace, Bitmap::new()));

            if let Some(item) = mailbox
                .acls
                .iter_mut()
                .find(|item| item.account_id == acl_account_id)
            {
                match op {
                    ModRightsOp::Replace => {
                        if !rights.is_empty() {
                            item.grants = rights;
                        } else {
                            mailbox
                                .acls
                                .retain(|item| item.account_id != acl_account_id);
                        }
                    }
                    ModRightsOp::Add => {
                        item.grants.union(&rights);
                    }
                    ModRightsOp::Remove => {
                        for right in rights {
                            item.grants.remove(right);
                        }
                        if item.grants.is_empty() {
                            mailbox
                                .acls
                                .retain(|item| item.account_id != acl_account_id);
                        }
                    }
                }
            } else if !rights.is_empty() {
                match op {
                    ModRightsOp::Add | ModRightsOp::Replace => {
                        mailbox.acls.push(AclGrant {
                            account_id: acl_account_id,
                            grants: rights,
                        });
                    }
                    ModRightsOp::Remove => (),
                }
            }

            if mailbox.acls.len() > data.server.core.groupware.max_shares_per_item {
                return Err(trc::ImapEvent::Error
                    .into_err()
                    .details("Maximum shares per item exceeded")
                    .caused_by(trc::location!()));
            }

            let grants = mailbox
                .acls
                .iter()
                .map(|r| trc::Value::from(r.account_id))
                .collect::<Vec<_>>();

            // Write changes
            let mut batch = BatchBuilder::new();
            batch
                .with_account_id(mailbox_id.account_id)
                .with_collection(Collection::Mailbox)
                .with_document(mailbox_id.mailbox_id)
                .custom(
                    ObjectIndexBuilder::new()
                        .with_changes(mailbox)
                        .with_current(current_mailbox),
                )
                .imap_ctx(&arguments.tag, trc::location!())?;

            if !batch.is_empty() {
                data.server
                    .commit_batch(batch)
                    .await
                    .imap_ctx(&arguments.tag, trc::location!())?;
            }

            // Invalidate ACLs
            data.server
                .invalidate_caches(CacheInvalidation::AccessToken(acl_account_id).into())
                .await
                .imap_ctx(&arguments.tag, trc::location!())?;

            trc::event!(
                Imap(trc::ImapEvent::SetAcl),
                SpanId = data.session_id,
                MailboxName = arguments.mailbox_name.clone(),
                AccountId = mailbox_id.account_id,
                MailboxId = mailbox_id.mailbox_id,
                Details = grants,
                Elapsed = op_start.elapsed()
            );

            data.write_bytes(
                StatusResponse::completed(command)
                    .with_tag(arguments.tag)
                    .into_bytes(),
            )
            .await
        })
    }

    pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
        // Validate access
        self.assert_has_permission(Permission::ImapListRights)?;

        let op_start = Instant::now();
        let arguments = request.parse_acl(self.is_utf8)?;

        trc::event!(
            Imap(trc::ImapEvent::ListRights),
            SpanId = self.session_id,
            MailboxName = arguments.mailbox_name.clone(),
            Elapsed = op_start.elapsed()
        );

        self.write_bytes(
            StatusResponse::completed(Command::ListRights)
                .with_tag(arguments.tag)
                .serialize(
                    ListRightsResponse {
                        mailbox_name: arguments.mailbox_name,
                        identifier: arguments.identifier.unwrap(),
                        permissions: vec![
                            vec![Rights::Read],
                            vec![Rights::Lookup],
                            vec![Rights::Write, Rights::Seen],
                            vec![Rights::Insert],
                            vec![Rights::Expunge, Rights::DeleteMessages],
                            vec![Rights::CreateMailbox],
                            vec![Rights::DeleteMailbox],
                            vec![Rights::Post],
                            vec![Rights::Administer],
                        ],
                    }
                    .into_bytes(self.version.is_rev2() || self.is_utf8),
                ),
        )
        .await
    }

    pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<bool> {
        match &self.state {
            State::Authenticated { data } | State::Selected { data, .. } => data
                .access_token
                .enforce_permission(permission)
                .map(|_| true),
            State::NotAuthenticated { .. } => Ok(false),
        }
    }
}

impl<T: SessionStream> SessionData<T> {
    async fn get_acl_mailbox(
        &self,
        arguments: &Arguments,
        validate: bool,
    ) -> trc::Result<(MailboxId, Archive<AlignedBytes>, AccessToken)> {
        if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) {
            if let Some(values) = self
                .server
                .store()
                .get_value::<Archive<AlignedBytes>>(ValueKey::archive(
                    mailbox.account_id,
                    Collection::Mailbox,
                    mailbox.mailbox_id,
                ))
                .await
                .caused_by(trc::location!())?
            {
                let access_token = self
                    .refresh_access_token()
                    .await
                    .caused_by(trc::location!())?;
                if !validate
                    || access_token.is_member(mailbox.account_id)
                    || values
                        .unarchive::<email::mailbox::Mailbox>()
                        .caused_by(trc::location!())?
                        .acls
                        .effective_acl(&access_token)
                        .contains(Acl::Share)
                {
                    Ok((mailbox, values, access_token))
                } else {
                    Err(trc::ImapEvent::Error
                        .into_err()
                        .details("You do not have enough permissions to perform this operation.")
                        .code(ResponseCode::NoPerm))
                }
            } else {
                Err(trc::ImapEvent::Error
                    .caused_by(trc::location!())
                    .details("Mailbox does not exist."))
            }
        } else {
            Err(trc::ImapEvent::Error
                .into_err()
                .details("Mailbox does not exist."))
        }
    }
}
