// 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 regex::Regex;
use tracing::trace;

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

pub static DOMAIN_LDAP_BASEDN_RE: LazyLock<Regex> = LazyLock::new(|| {
    #[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)
    }
}

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. This was the behaviour in versions
                // prior to DL10. Rather than checking the domain version itself, the issue is we
                // have to check the min remigration level. This is because during a server setup
                // we start from the MIN remigration level and work up, and the domain version == 0.
                //
                // So effectively we only skip setting this value after we know that we are at DL12
                // since we could never go back to anything lower than 10 at that point.
                if DOMAIN_MIN_REMIGRATION_LEVEL < DOMAIN_LEVEL_10
                    && !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));
                }

                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)));
    }
}
