// Manage and generate domain uuid's and/or trust related domain
// management.

// The primary point of this is to generate a unique domain UUID on startup
// which is importart for management of the replication topo and trust
// relationships.
use std::iter::once;
use std::sync::Arc;

use compact_jwt::JwsEs256Signer;
use rand::prelude::*;
use regex::Regex;
use tracing::trace;

use crate::event::{CreateEvent, ModifyEvent};
use crate::plugins::Plugin;
use crate::prelude::*;

lazy_static! {
    pub static ref DOMAIN_LDAP_BASEDN_RE: Regex = {
        #[allow(clippy::expect_used)]
        Regex::new(r"^(dc|o|ou)=[a-z][a-z0-9]*(,(dc|o|ou)=[a-z][a-z0-9]*)*$")
            .expect("Invalid domain ldap basedn regex")
    };
}

pub struct Domain {}

impl Plugin for Domain {
    fn id() -> &'static str {
        "plugin_domain"
    }

    #[instrument(level = "debug", name = "domain_pre_create_transform", skip_all)]
    fn pre_create_transform(
        qs: &mut QueryServerWriteTransaction,
        cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
        _ce: &CreateEvent,
    ) -> Result<(), OperationError> {
        Self::modify_inner(qs, cand)
    }

    #[instrument(level = "debug", name = "domain_pre_modify", skip_all)]
    fn pre_modify(
        qs: &mut QueryServerWriteTransaction,
        _pre_cand: &[Arc<EntrySealedCommitted>],
        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
        _me: &ModifyEvent,
    ) -> Result<(), OperationError> {
        Self::modify_inner(qs, cand)
    }

    #[instrument(level = "debug", name = "domain_pre_batch_modify", skip_all)]
    fn pre_batch_modify(
        qs: &mut QueryServerWriteTransaction,
        _pre_cand: &[Arc<EntrySealedCommitted>],
        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
        _me: &BatchModifyEvent,
    ) -> Result<(), OperationError> {
        Self::modify_inner(qs, cand)
    }
}

fn generate_domain_cookie_key() -> Value {
    let mut key = [0; 64];
    let mut rng = StdRng::from_entropy();
    rng.fill(&mut key);
    Value::new_privatebinary(&key)
}

impl Domain {
    /// Generates the cookie key for the domain.

    fn modify_inner<T: Clone + std::fmt::Debug>(
        qs: &mut QueryServerWriteTransaction,
        cand: &mut [Entry<EntryInvalid, T>],
    ) -> Result<(), OperationError> {
        cand.iter_mut().try_for_each(|e| {
            if e.attribute_equality(Attribute::Class, &EntryClass::DomainInfo.into())
                && e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO)
            {
                // Validate the domain ldap basedn syntax.
                if let Some(basedn) = e
                    .get_ava_single_iutf8(Attribute::DomainLdapBasedn) {

                    if !DOMAIN_LDAP_BASEDN_RE.is_match(basedn) {
                        error!("Invalid {} '{}'. Must pass regex \"{}\"", Attribute::DomainLdapBasedn,basedn, *DOMAIN_LDAP_BASEDN_RE);
                        return Err(OperationError::InvalidState);
                    }
                }

                // We always set this, because the DB uuid is authoritative.
                let u = Value::Uuid(qs.get_domain_uuid());
                e.set_ava(&Attribute::DomainUuid, once(u));
                trace!("plugin_domain: Applying uuid transform");

                // We only apply this if one isn't provided.
                if !e.attribute_pres(Attribute::DomainName) {
                    let n = Value::new_iname(qs.get_domain_name());
                    e.set_ava(&Attribute::DomainName, once(n));
                    trace!("plugin_domain: Applying domain_name transform");
                }

                // Setup the minimum functional level if one is not set already.
                if !e.attribute_pres(Attribute::Version) {
                    let n = Value::Uint32(DOMAIN_LEVEL_0);
                    e.set_ava(&Attribute::Version, once(n));
                    warn!("plugin_domain: Applying domain version transform");
                } else {
                    debug!("plugin_domain: NOT Applying domain version transform");
                };

                // create the domain_display_name if it's missing
                if !e.attribute_pres(Attribute::DomainDisplayName) {
                    let domain_display_name = Value::new_utf8(format!("Kanidm {}", qs.get_domain_name()));
                    security_info!("plugin_domain: setting default domain_display_name to {:?}", domain_display_name);

                    e.set_ava(&Attribute::DomainDisplayName, once(domain_display_name));
                }

                if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::FernetPrivateKeyStr) {
                    security_info!("regenerating domain token encryption key");
                    let k = fernet::Fernet::generate_key();
                    let v = Value::new_secret_str(&k);
                    e.add_ava(Attribute::FernetPrivateKeyStr, v);
                }

                if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::Es256PrivateKeyDer) {
                    security_info!("regenerating domain es256 private key");
                    let der = JwsEs256Signer::generate_es256()
                        .and_then(|jws| jws.private_key_to_der())
                        .map_err(|e| {
                            admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key");
                            OperationError::CryptographyError
                        })?;
                    let v = Value::new_privatebinary(&der);
                    e.add_ava(Attribute::Es256PrivateKeyDer, v);
                }

                if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::PrivateCookieKey) {
                    security_info!("regenerating domain cookie key");
                    e.add_ava(Attribute::PrivateCookieKey, generate_domain_cookie_key());
                }

                trace!(?e);
                Ok(())
            } else {
                Ok(())
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::prelude::*;

    // test we can create and generate the id
    #[qs_test]
    async fn test_domain_generate_uuid(server: &QueryServer) {
        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
        let e_dom = server_txn
            .internal_search_uuid(UUID_DOMAIN_INFO)
            .expect("must not fail");

        let u_dom = server_txn.get_domain_uuid();

        assert!(e_dom.attribute_equality(Attribute::DomainUuid, &PartialValue::Uuid(u_dom)));
    }
}
