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

use super::{PrincipalInfo, manage::ManageDirectory};
use crate::{Principal, PrincipalData, QueryBy, QueryParams, Type, backend::RcptType};
use mail_send::Credentials;
use store::{
    Deserialize, IterateParams, Store, ValueKey,
    write::{DirectoryClass, ValueClass},
};
use trc::AddContext;
use utils::DomainPart;

#[allow(async_fn_in_trait)]
pub trait DirectoryStore: Sync + Send {
    async fn query(&self, by: QueryParams<'_>) -> trc::Result<Option<Principal>>;
    async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>>;
    async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>;
    async fn rcpt(&self, address: &str) -> trc::Result<RcptType>;
    async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>>;
    async fn expn(&self, address: &str) -> trc::Result<Vec<String>>;
    async fn expn_by_id(&self, id: u32) -> trc::Result<Vec<String>>;
}

impl DirectoryStore for Store {
    async fn query(&self, by: QueryParams<'_>) -> trc::Result<Option<Principal>> {
        let (account_id, secret) = match by.by {
            QueryBy::Name(name) => (self.get_principal_id(name).await?, None),
            QueryBy::Id(account_id) => (account_id.into(), None),
            QueryBy::Credentials(credentials) => match credentials {
                Credentials::Plain { username, secret } => (
                    self.get_principal_id(username).await?,
                    secret.as_str().into(),
                ),
                Credentials::OAuthBearer { token } => {
                    (self.get_principal_id(token).await?, token.as_str().into())
                }
                Credentials::XOauth2 { username, secret } => (
                    self.get_principal_id(username).await?,
                    secret.as_str().into(),
                ),
            },
        };

        if let Some(account_id) = account_id
            && let Some(mut principal) = self.get_principal(account_id).await?
        {
            if let Some(secret) = secret
                && !principal
                    .verify_secret(secret, by.only_app_pass, true)
                    .await?
            {
                return Ok(None);
            }

            if by.return_member_of {
                for member in self.get_member_of(principal.id).await? {
                    match member.typ {
                        Type::List => principal
                            .data
                            .push(PrincipalData::List(member.principal_id)),
                        Type::Role => principal
                            .data
                            .push(PrincipalData::Role(member.principal_id)),
                        _ => principal
                            .data
                            .push(PrincipalData::MemberOf(member.principal_id)),
                    }
                }
            }
            return Ok(Some(principal));
        }
        Ok(None)
    }

    async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>> {
        self.get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory(
            DirectoryClass::EmailToId(address.as_bytes().to_vec()),
        )))
        .await
        .map(|ptype| ptype.map(|ptype| ptype.id))
    }

    async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
        self.get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory(
            DirectoryClass::NameToId(domain.as_bytes().to_vec()),
        )))
        .await
        .map(|p| p.is_some_and(|p| p.typ == Type::Domain))
    }

    async fn rcpt(&self, address: &str) -> trc::Result<RcptType> {
        if let Some(pinfo) = self
            .get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory(
                DirectoryClass::EmailToId(address.as_bytes().to_vec()),
            )))
            .await?
        {
            if pinfo.typ != Type::List {
                Ok(RcptType::Mailbox)
            } else {
                self.expn_by_id(pinfo.id).await.map(RcptType::List)
            }
        } else {
            Ok(RcptType::Invalid)
        }
    }

    async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
        let mut results = Vec::new();
        let address = address.try_local_part().unwrap_or(address);
        if address.len() > 3 {
            self.iterate(
                IterateParams::new(
                    ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId(vec![0u8]))),
                    ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId(
                        vec![u8::MAX; 10],
                    ))),
                ),
                |key, value| {
                    let key =
                        std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default();
                    if key.try_local_part().unwrap_or(key).contains(address)
                        && PrincipalInfo::deserialize(value)
                            .caused_by(trc::location!())?
                            .typ
                            != Type::List
                    {
                        results.push(key.into());
                    }
                    Ok(true)
                },
            )
            .await
            .caused_by(trc::location!())?;
        }

        Ok(results)
    }

    async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
        if let Some(ptype) = self
            .get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory(
                DirectoryClass::EmailToId(address.as_bytes().to_vec()),
            )))
            .await?
            .filter(|p| p.typ == Type::List)
        {
            self.expn_by_id(ptype.id).await
        } else {
            Ok(vec![])
        }
    }

    async fn expn_by_id(&self, list_id: u32) -> trc::Result<Vec<String>> {
        let mut results = Vec::new();
        for account_id in self.get_members(list_id).await? {
            if let Some(email) = self.get_principal(account_id).await?.and_then(|p| {
                p.data.into_iter().find_map(|data| {
                    if let PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) =
                        data
                    {
                        Some(email)
                    } else {
                        None
                    }
                })
            }) {
                results.push(email);
            }
        }

        if let Some(principal) = self.get_principal(list_id).await? {
            results.extend(principal.data.into_iter().filter_map(|data| {
                if let PrincipalData::ExternalMember(member) = data {
                    Some(member)
                } else {
                    None
                }
            }));
        }

        Ok(results)
    }
}
