/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.causeway.extensions.secman.applib.user.dom;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.apache.causeway.applib.annotation.Collection;
import org.apache.causeway.applib.annotation.CollectionLayout;
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.DomainObjectLayout;
import org.apache.causeway.applib.annotation.Editing;
import org.apache.causeway.applib.annotation.MemberSupport;
import org.apache.causeway.applib.annotation.ObjectSupport;
import org.apache.causeway.applib.annotation.Optionality;
import org.apache.causeway.applib.annotation.Parameter;
import org.apache.causeway.applib.annotation.ParameterLayout;
import org.apache.causeway.applib.annotation.Programmatic;
import org.apache.causeway.applib.annotation.Property;
import org.apache.causeway.applib.annotation.PropertyLayout;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.mixins.security.HasUsername;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureId;
import org.apache.causeway.applib.services.user.RoleMemento;
import org.apache.causeway.applib.services.user.UserMemento;
import org.apache.causeway.applib.services.user.UserService;
import org.apache.causeway.applib.util.ObjectContracts;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.core.config.CausewayConfiguration;
import org.apache.causeway.core.config.CausewayConfiguration.Extensions.Secman;
import org.apache.causeway.extensions.secman.applib.CausewayModuleExtSecmanApplib;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionMode;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRepository;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionValueSet;
import org.apache.causeway.extensions.secman.applib.permission.spi.PermissionsEvaluationService;
import org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole;
import org.apache.causeway.extensions.secman.applib.tenancy.dom.HasAtPath;

import lombok.experimental.UtilityClass;

/**
 * @since 2.0 {@index}
 */
@Named(ApplicationUser.LOGICAL_TYPE_NAME)
@DomainObject(
        autoCompleteRepository = ApplicationUserRepository.class,
        autoCompleteMethod = "findMatching"
        )
@DomainObjectLayout(
        titleUiEvent = ApplicationUser.TitleUiEvent.class,
        iconUiEvent = ApplicationUser.IconUiEvent.class,
        cssClassUiEvent = ApplicationUser.CssClassUiEvent.class,
        layoutUiEvent = ApplicationUser.LayoutUiEvent.class
)
public abstract class ApplicationUser
        implements HasUsername, HasAtPath, Comparable<ApplicationUser> {

    public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".ApplicationUser";
    public static final String SCHEMA = CausewayModuleExtSecmanApplib.SCHEMA;
    public static final String TABLE = "ApplicationUser";

    @UtilityClass
    public static class Nq {
        public static final String FIND_BY_USERNAME = LOGICAL_TYPE_NAME + ".findByUsername";
        public static final String FIND_BY_EMAIL_ADDRESS = LOGICAL_TYPE_NAME + ".findByEmailAddress";
        public static final String FIND = LOGICAL_TYPE_NAME + ".find";
        public static final String FIND_BY_ATPATH = LOGICAL_TYPE_NAME + ".findByAtPath";
    }

    // -- UI & DOMAIN EVENTS

    public static class TitleUiEvent extends CausewayModuleExtSecmanApplib.TitleUiEvent<ApplicationUser> { }
    public static class IconUiEvent extends CausewayModuleExtSecmanApplib.IconUiEvent<ApplicationUser> { }
    public static class CssClassUiEvent extends CausewayModuleExtSecmanApplib.CssClassUiEvent<ApplicationUser> { }
    public static class LayoutUiEvent extends CausewayModuleExtSecmanApplib.LayoutUiEvent<ApplicationUser> { }

    public static abstract class PropertyDomainEvent<T> extends CausewayModuleExtSecmanApplib.PropertyDomainEvent<ApplicationUser, T> {}
    public static abstract class CollectionDomainEvent<T> extends CausewayModuleExtSecmanApplib.CollectionDomainEvent<ApplicationUser, T> {}

    @Inject private transient ApplicationUserRepository applicationUserRepository;
    @Inject private transient ApplicationPermissionRepository applicationPermissionRepository;
    @Inject private transient UserService userService;
    @Inject private transient PermissionsEvaluationService permissionsEvaluationService;
    @Inject private transient CausewayConfiguration config;

    @Programmatic protected ApplicationUserRepository getApplicationUserRepository() {
        return applicationUserRepository;
    }

    @Programmatic protected ApplicationPermissionRepository getApplicationPermissionRepository() {
        return applicationPermissionRepository;
    }

    @Programmatic protected UserService getUserService() {
        return userService;
    }

    /**
     * Optional service, if configured then is used to evaluate permissions within
     * {@link ApplicationPermissionValueSet#evaluate(ApplicationFeatureId, ApplicationPermissionMode)}
     * else will fallback to a default implementation.
     */
    @Programmatic protected PermissionsEvaluationService getPermissionsEvaluationService() {
        return permissionsEvaluationService;
    }

    @Programmatic protected Secman getSecmanConfig() {
        return config.getExtensions().getSecman();
    }

    @ObjectSupport public String title() {
        return getName();
    }
    @ObjectSupport public String iconName() {
        return ApplicationUserStatus.isUnlocked(getStatus()) ? "unlocked" : "locked";
    }

    // -- NAME

    @Property(
            domainEvent = Name.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            hidden= Where.OBJECT_FORMS,
            fieldSetId = "identity",
            sequence = "1"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Name {
        class DomainEvent extends PropertyDomainEvent<String> {}
    }

    @Name
    public String getName() {
        final StringBuilder buf = new StringBuilder();
        if(getFamilyName() != null) {
            if(getKnownAs() != null) {
                buf.append(getKnownAs());
            } else {
                buf.append(getGivenName());
            }
            buf.append(' ')
                    .append(getFamilyName())
                    .append(" (").append(getUsername()).append(')');
        } else {
            buf.append(getUsername());
        }
        return buf.toString();
    }

    // -- USERNAME

    @Property(
            domainEvent = Username.DomainEvent.class
    )
    @PropertyLayout(
            fieldSetId="identity",
            hidden = Where.PARENTED_TABLES,
            sequence = "1"
    )
    @HasUsername.Username
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Username {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = HasUsername.Username.MAX_LENGTH;
        boolean NULLABLE = HasUsername.Username.NULLABLE;
        String ALLOWS_NULL = HasUsername.Username.ALLOWS_NULL;
    }
    @Override
    @Username
    public abstract String getUsername();
    public abstract void setUsername(String username);

    // -- FAMILY NAME

    @Property(
            domainEvent = FamilyName.DomainEvent.class,
            editing = Editing.DISABLED,
            maxLength = FamilyName.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @PropertyLayout(
            fieldSetId = "name",
            hidden = Where.ALL_TABLES,
            sequence = "2.1"
    )
    @Parameter(
            maxLength = FamilyName.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @ParameterLayout(
            named="Family Name"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FamilyName {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = 120;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @FamilyName
    public abstract String getFamilyName();
    public abstract void setFamilyName(String familyName);

    // -- GIVEN NAME

    @Property(
            domainEvent = GivenName.DomainEvent.class,
            editing = Editing.DISABLED,
            maxLength = GivenName.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @PropertyLayout(
            fieldSetId = "name",
            hidden = Where.ALL_TABLES,
            sequence = "2.2"
    )
    @Parameter(
            maxLength = GivenName.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @ParameterLayout(
            named="Given Name"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface GivenName {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = 120;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @GivenName
    public abstract String getGivenName();
    public abstract void setGivenName(String givenName);

    // -- KNOWN AS

    @Property(
            domainEvent = KnownAs.KnownAsDomainEvent.class,
            editing = Editing.DISABLED,
            maxLength = KnownAs.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @PropertyLayout(
            fieldSetId = "name",
            hidden = Where.ALL_TABLES,
            sequence = "2.3"
    )
    @Parameter(
            maxLength = KnownAs.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @ParameterLayout(
            named="Known As"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface KnownAs {
        class KnownAsDomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = 120;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @KnownAs
    public abstract String getKnownAs();
    public abstract void setKnownAs(String knownAs);

    // -- EMAIL ADDRESS

    @Property(
            domainEvent = EmailAddress.DomainEvent.class,
            editing = Editing.DISABLED,
            maxLength = EmailAddress.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @PropertyLayout(
            fieldSetName = "Contact Details",
            sequence = "3.1"
    )
    @Parameter(
            maxLength = EmailAddress.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @ParameterLayout(
            named = "Email"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EmailAddress {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = 120;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @EmailAddress
    public abstract String getEmailAddress();
    public abstract void setEmailAddress(String emailAddress);

    // -- PHONE NUMBER

    @Property(
            domainEvent = PhoneNumber.DomainEvent.class,
            editing = Editing.DISABLED,
            maxLength = PhoneNumber.MAX_LENGTH
    )
    @PropertyLayout(
            fieldSetName = "Contact Details",
            sequence = "3.2"
    )
    @Parameter(
            maxLength = PhoneNumber.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @ParameterLayout(
            named = "Phone"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PhoneNumber {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = 120;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @PhoneNumber
    public abstract String getPhoneNumber();
    public abstract void setPhoneNumber(String phoneNumber);

    // -- FAX NUMBER

    @Property(
            domainEvent = FaxNumber.DomainEvent.class,
            editing = Editing.DISABLED,
            maxLength = FaxNumber.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @PropertyLayout(
            fieldSetName = "Contact Details",
            hidden = Where.PARENTED_TABLES,
            sequence = "3.3"
    )
    @Parameter(
            maxLength = FaxNumber.MAX_LENGTH,
            optionality = Optionality.OPTIONAL
    )
    @ParameterLayout(
            named = "Fax"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FaxNumber {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = 120;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @FaxNumber
    public abstract String getFaxNumber();
    public abstract void setFaxNumber(String faxNumber);

    // -- LOCALEs

    @Property(
            domainEvent = Locale.DomainEvent.class,
            editing = Editing.DISABLED, //  edit via update button
            optionality = Optionality.OPTIONAL
    )
    @PropertyLayout(
            fieldSetId = "regional"
    )
    @Parameter(
            optionality = Optionality.OPTIONAL
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Locale {
        class DomainEvent extends PropertyDomainEvent<java.util.Locale> {}
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }

    @Property(
            domainEvent = Locale.DomainEvent.class
    )
    @Locale
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Language {
        class DomainEvent extends Locale.DomainEvent {}
        boolean NULLABLE = Locale.NULLABLE;
        String ALLOWS_NULL = Locale.ALLOWS_NULL;
    }
    @Language
    public abstract java.util.Locale getLanguage();
    public abstract void setLanguage(java.util.Locale locale);

    @Property(
            domainEvent = NumberFormat.DomainEvent.class
    )
    @Locale
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NumberFormat {
        class DomainEvent extends Locale.DomainEvent {}
        boolean NULLABLE = Locale.NULLABLE;
        String ALLOWS_NULL = Locale.ALLOWS_NULL;
    }
    @NumberFormat
    public abstract java.util.Locale getNumberFormat();
    public abstract void setNumberFormat(java.util.Locale locale);

    @Property(
            domainEvent = TimeFormat.DomainEvent.class
    )
    @Locale
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TimeFormat {
        class DomainEvent extends Locale.DomainEvent {}
        boolean NULLABLE = Locale.NULLABLE;
        String ALLOWS_NULL = Locale.ALLOWS_NULL;
    }
    @TimeFormat
    public abstract java.util.Locale getTimeFormat();
    public abstract void setTimeFormat(java.util.Locale locale);

    @Property(
            domainEvent = AtPath.DomainEvent.class
    )
    @PropertyLayout(
            fieldSetId = "access",
            sequence = "4"
    )
    @HasAtPath.AtPath
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AtPath {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = HasAtPath.AtPath.MAX_LENGTH;
        boolean NULLABLE = HasAtPath.AtPath.NULLABLE;
        String ALLOWS_NULL = HasAtPath.AtPath.ALLOWS_NULL;
    }
    @Override
    @AtPath
    public abstract String getAtPath();
    public abstract void setAtPath(String atPath);

    // -- ACCOUNT TYPE

    @Property(
            domainEvent = AccountType.DomainEvent.class,
            editing = Editing.DISABLED,
            optionality = Optionality.MANDATORY
    )
    @PropertyLayout(
            fieldSetId = "access",
            sequence = "2"
    )
    @Parameter(
            optionality = Optionality.MANDATORY
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AccountType {
        class DomainEvent extends PropertyDomainEvent<AccountType> {}
        boolean NULLABLE = false;
        String ALLOWS_NULL = "false";
    }
    @AccountType
    public abstract org.apache.causeway.extensions.secman.applib.user.dom.AccountType getAccountType();
    public abstract void setAccountType(org.apache.causeway.extensions.secman.applib.user.dom.AccountType accountType);

    // -- STATUS

    @Property(
            domainEvent = Status.DomainEvent.class,
            editing = Editing.DISABLED,
            optionality = Optionality.MANDATORY
    )
    @PropertyLayout(
            fieldSetId = "access",
            sequence = "1"
    )
    @Parameter(
            optionality = Optionality.MANDATORY
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Status {
        class DomainEvent extends PropertyDomainEvent<ApplicationUserStatus> {}
        boolean NULLABLE = false;
        String ALLOWS_NULL = "false";
    }
    @Status
    public abstract ApplicationUserStatus getStatus();
    public abstract void setStatus(ApplicationUserStatus disabled);

    // -- ENCRYPTED PASSWORD

    @Property
    @PropertyLayout(hidden = Where.EVERYWHERE)
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EncryptedPassword {
        int MAX_LENGTH = 255;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @EncryptedPassword
    public abstract String getEncryptedPassword();
    public abstract void setEncryptedPassword(String encryptedPassword);
    @MemberSupport public boolean hideEncryptedPassword() {
        return !getApplicationUserRepository().isPasswordFeatureEnabled(this);
    }

    // -- HAS PASSWORD

    @Property(
            domainEvent = HasPassword.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            fieldSetId = "access",
            named = "Has password?",
            sequence = "3"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface HasPassword {
        class DomainEvent extends PropertyDomainEvent<Boolean> {}
    }

    @HasPassword
    public boolean isHasPassword() {
        return _Strings.isNotEmpty(getEncryptedPassword());
    }
    @MemberSupport public boolean hideHasPassword() {
        return !getApplicationUserRepository().isPasswordFeatureEnabled(this);
    }

    // ROLES

    @Collection(
            domainEvent = Roles.RolesDomainEvent.class
    )
    @CollectionLayout(
            defaultView="table",
            sequence = "1"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Roles {
        class RolesDomainEvent extends CollectionDomainEvent<ApplicationRole> {}

        @UtilityClass
        class Persistence {
            public static final String TABLE = "ApplicationUserRoles";
            public static final String JOIN_COLUMN = "userId";
            public static final String INVERSE_JOIN_COLUMN = "roleId";
        }
    }

    @Roles
    public abstract Set<ApplicationRole> getRoles();

    // -- PERMISSION SET

    // short-term caching
    private transient ApplicationPermissionValueSet cachedPermissionSet;

    @Programmatic public ApplicationPermissionValueSet getPermissionSet() {
        if(cachedPermissionSet != null) {
            return cachedPermissionSet;
        }
        List<ApplicationPermission> permissions;
        if(userService.isImpersonating()) {
            permissions = getApplicationPermissionRepository().findByUserMemento(userService.getUser());
        } else {
            permissions = getApplicationPermissionRepository().findByUser(this);
        }
        return cachedPermissionSet =
                new ApplicationPermissionValueSet(
                        _Lists.map(_Casts.uncheckedCast(permissions), ApplicationPermission.Functions.AS_VALUE),
                        getPermissionsEvaluationService());
    }

    // -- IS FOR SELF OR RUN AS ADMINISTRATOR

    @Programmatic public boolean isForSelf() {
        var currentUser = currentUser();
        var currentUserName = currentUser.name();
        var forSelf = Objects.equals(getUsername(), currentUserName);
        return forSelf;
    }

    @Programmatic public boolean isRunAsAdministrator() {
        var currentUser = currentUser();
        var adminRoleName = getAdminRoleName(); // is guarded to not be empty
        var adminRoleSuffix = ":" + adminRoleName;
        for (final RoleMemento role : currentUser.roles()) {
            final String roleName = role.name();
            if(adminRoleName.equals(roleName)) return true;
            
            // format could also be realmName:roleName, eg. with Shiro
            // since we don't know what the realm's name is (depends on its configuration in shiro.ini),
            // simply check that the last part matches the role name.
            if(roleName.endsWith(adminRoleSuffix)) return true;
        }
        return false;
    }

    @Programmatic public boolean isForSelfOrRunAsAdministrator() {
        return isForSelf()
                || isRunAsAdministrator();
    }

    // -- HELPERS

    @Programmatic public boolean isLocalAccount() {
        return getAccountType() == org.apache.causeway.extensions.secman.applib.user.dom.AccountType.LOCAL;
    }

    @Programmatic private String getAdminRoleName() {
        var adminRoleName = _Strings.emptyToNull(getSecmanConfig().getSeed().getAdmin().getRoleName());
        // guard against empty admin role name
        _Assert.assertNotNull(adminRoleName, ()->"secman-config.seed.admin.role-name must not be empty");
        return adminRoleName;
    }

    @Programmatic private UserMemento currentUser() {
        return getUserService().currentUserElseFail();
    }

    // -- equals, hashCode, compareTo, toString
    private static final String propertyNames = "username";

    private static final ObjectContracts.ObjectContract<ApplicationUser> contract =
            ObjectContracts.parse(ApplicationUser.class, propertyNames);

    @Override
    public int compareTo(final org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser other) {
        return contract.compare(this, other);
    }

    @Override
    public boolean equals(final Object obj) {
        return contract.equals(this, obj);
    }

    @Override
    public int hashCode() {
        return contract.hashCode(this);
    }

    @Override
    public String toString() {
        return contract.toString(this);
    }

}
