package com.airlock.iam.admin.infrastructure.mapper.prot;

import ch.ergon.restal.jsonapi.Metadata;
import ch.ergon.restal.jsonapi.document.data.Relationship;
import ch.ergon.restal.jsonapi.document.data.ResourceIdentifier;
import ch.ergon.restal.jsonapi.document.data.Relationship.ToManyRelationship;
import ch.ergon.restal.jsonapi.document.data.Relationship.ToOneRelationship;
import com.airlock.iam.admin.application.service.token.vasco.VascoOtpDeviceId;
import com.airlock.iam.admin.domain.model.maintenancemessage.MaintenanceMessage;
import com.airlock.iam.admin.domain.model.token.oathotp.OathOtpTokenQrCodeView;
import com.airlock.iam.admin.domain.model.token.vasco.VascoOtpAssignment;
import com.airlock.iam.admin.domain.model.token.vasco.VascoOtpDevice;
import com.airlock.iam.admin.domain.model.token.vasco.VascoOtpDeviceUserView;
import com.airlock.iam.admin.infrastructure.transfer.request.MaintenanceMessageRequest;
import com.airlock.iam.admin.infrastructure.transfer.request.user.UserLockRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.LicenseData;
import com.airlock.iam.admin.infrastructure.transfer.response.LicenseValidationWarningData;
import com.airlock.iam.admin.infrastructure.transfer.response.LogsData;
import com.airlock.iam.admin.infrastructure.transfer.response.MaintenanceMessageData;
import com.airlock.iam.admin.infrastructure.transfer.response.SmsDeliveryStatusData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.LockData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.UserData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.UserStatisticsData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.CrontoActivationCodeData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.CrontoConfidentialActivationData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.CrontoDeviceData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.EnforcePasswordChangeData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.OrderPasswordData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.SecretQuestionData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserAuthenticationMethodData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserHotpTokenData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserMtanTokenData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserOathOtpTokenSecretData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserPasswordData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserTotpTokenData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserVascoDeviceData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.UserVascoTokenActivationLetterOrderStatusData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.VascoAssigneeRelationshipMetadata;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.VascoDeviceData;
import com.airlock.iam.base.infrastructure.mapper.jsonapi.ResourceObjectMetaData;
import com.airlock.iam.base.misc.date.LocalDateHelper;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.domain.model.logs.Log;
import com.airlock.iam.common.domain.model.mtan.MtanItemView;
import com.airlock.iam.common.domain.model.token.oathotp.HotpToken;
import com.airlock.iam.common.domain.model.token.oathotp.TotpToken;
import com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestion;
import com.airlock.iam.common.infrastructure.mapper.ConverterId;
import com.airlock.iam.core.misc.cronto.Cryptogram;
import com.airlock.iam.core.misc.cronto.CrontoHandler.PlatformType;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoLetterTokenData;
import com.airlock.iam.core.misc.persistency.AuthUserBean;
import com.airlock.iam.core.misc.sms.SmsGateway.DeliveryStatus;
import com.airlock.iam.license.domain.model.verification.input.License;
import com.airlock.iam.license.domain.model.verification.warnings.LicenseWarning;
import com.google.common.collect.Maps;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import ma.glasnost.orika.CustomMapper;
import ma.glasnost.orika.DefaultFieldMapper;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.MappingContext;
import ma.glasnost.orika.metadata.ClassMapBuilder;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.Validate;

public final class AdminRestBeanMappings {
   public AdminRestBeanMappings(MapperFactory factory) {
      this.configureAll(factory);
   }

   private void configureAll(MapperFactory factory) {
      this.configureUser(factory);
      this.configureUserLockRequest(factory);
      this.configureUserAuthenticationMethod(factory);
      this.configureUserStatistics(factory);
      this.configureUserPasswordData(factory);
      this.configureUserMtan(factory);
      this.configureUserOathOtp(factory);
      this.configureUserCronto(factory);
      this.configureUserVasco(factory);
      this.configureSecretQuestionData(factory);
      this.configureSmsDeliveryStatus(factory);
      this.configureLicense(factory);
      this.configureLog(factory);
      this.configureMaintenanceMessage(factory);
   }

   private void configureUser(MapperFactory factory) {
      factory.classMap(PersistentUser.class, UserData.class)
         .field("", "statistics")
         .field("", "authenticationMethod")
         .exclude("contextData")
         .byDefault(new DefaultFieldMapper[0])
         .customize(new AdminRestBeanMappings.PersistentUserUserDataMapper())
         .register();
   }

   private void configureUserLockRequest(MapperFactory factory) {
      factory.classMap(UserLockRequest.class, PersistentUser.class).byDefault(new DefaultFieldMapper[0]).field("reason", "lockReason").register();
   }

   private void configureUserAuthenticationMethod(MapperFactory factory) {
      factory.classMap(PersistentUser.class, UserAuthenticationMethodData.class)
         .field("authMethod", "activeMethod")
         .customize(new AdminRestBeanMappings.PersistentUserUserAuthenticationMethodMapper())
         .register();
   }

   private void configureUserStatistics(MapperFactory factory) {
      factory.classMap(PersistentUser.class, UserStatisticsData.class)
         .field("", "loginHistory")
         .field("", "selfService")
         .field("lastKnownGlobalSessionID", "loginHistory.lastAirlockSessionId")
         .field("lastGlobalSessionIDUpdate", "loginHistory.lastAirlockSessionIdUpdate")
         .field("allFailedLogins.failedLoginCounters", "loginHistory.failedLoginsByAuthenticationMethod")
         .field("allFailedLogins.latestFailedLoginAttempts", "loginHistory.latestFailedLoginAttemptsByAuthenticationMethod")
         .byDefault(new DefaultFieldMapper[0])
         .register();
   }

   private void configureUserPasswordData(MapperFactory factory) {
      factory.classMap(AuthUserBean.class, UserPasswordData.class)
         .fieldAToB("passwordPresent:{existsPasswordForUser| }", "passwordPresent")
         .field("latestPasswordChangeTimestamp", "mostRecentPasswordChange")
         .field("passwordGenerationDate", "mostRecentPasswordGeneration")
         .field("", "orderPassword")
         .field("", "enforcePasswordChange")
         .byDefault(new DefaultFieldMapper[0])
         .register();
      factory.classMap(AuthUserBean.class, OrderPasswordData.class)
         .field("passwordOrderDate", "orderDate")
         .field("passwordOrderUser", "issuer")
         .field("orderPasswordFlag", "ordered")
         .byDefault(new DefaultFieldMapper[0])
         .register();
      factory.classMap(AuthUserBean.class, EnforcePasswordChangeData.class)
         .field("nextEnforcedChangeTimestamp", "changeAfter")
         .field("passwordChangeForced", "enforceChange")
         .byDefault(new DefaultFieldMapper[0])
         .register();
   }

   private void configureUserMtan(MapperFactory factory) {
      factory.classMap(MtanItemView.class, UserMtanTokenData.class).field("sendSmsAsFlash", "flash").byDefault(new DefaultFieldMapper[0]).register();
   }

   private void configureUserOathOtp(MapperFactory factory) {
      factory.classMap(TotpToken.class, UserTotpTokenData.class)
         .byDefault(new DefaultFieldMapper[0])
         .customize(new AdminRestBeanMappings.TotpTokenMapper())
         .register();
      factory.classMap(HotpToken.class, UserHotpTokenData.class)
         .byDefault(new DefaultFieldMapper[0])
         .customize(new AdminRestBeanMappings.HotpTokenMapper())
         .register();
   }

   private void configureUserCronto(MapperFactory factory) {
      factory.classMap(CrontoDeviceToken.class, CrontoDeviceData.class)
         .byDefault(new DefaultFieldMapper[0])
         .customize(new AdminRestBeanMappings.CrontoDeviceTokenMapper())
         .register();
      factory.classMap(CrontoLetterTokenData.class, CrontoActivationCodeData.class)
         .byDefault(new DefaultFieldMapper[0])
         .customize(new AdminRestBeanMappings.CrontoLetterTokenMapper())
         .register();
   }

   private void configureUserVasco(MapperFactory factory) {
      factory.classMap(VascoOtpDeviceUserView.class, UserVascoDeviceData.class).customize(new AdminRestBeanMappings.UserVascoDeviceUserViewMapper()).register();
      factory.classMap(VascoOtpDeviceUserView.class, UserVascoTokenActivationLetterOrderStatusData.class)
         .customize(new AdminRestBeanMappings.UserVascoTokenActivationLetterOrderStatusMapper())
         .register();
      factory.classMap(VascoOtpDeviceUserView.class, ResourceObjectMetaData.class)
         .customize(new AdminRestBeanMappings.VascoOtpDeviceUserViewMapping())
         .register();
      factory.classMap(VascoOtpDevice.class, VascoDeviceData.class).customize(new AdminRestBeanMappings.VascoOtpDeviceMapping()).register();
      factory.classMap(VascoOtpDevice.class, ResourceObjectMetaData.class).customize(new AdminRestBeanMappings.VascoOtpDeviceMetaDataMapping()).register();
   }

   private static String[] getAllowedPlatforms(CrontoLetterTokenData letterToken) {
      return letterToken.allowedPlatformTypes().stream().map(PlatformType::getIdentifier).toArray(String[]::new);
   }

   private void configureMaintenanceMessage(MapperFactory factory) {
      this.configureMaintenanceMessageData(factory.classMap(MaintenanceMessage.class, MaintenanceMessageRequest.class));
      this.configureMaintenanceMessageData(factory.classMap(MaintenanceMessage.class, MaintenanceMessageData.class));
   }

   private void configureMaintenanceMessageData(ClassMapBuilder<MaintenanceMessage, ?> b) {
      b.fieldMap("location", "location")
         .mapNulls(false)
         .mapNullsInReverse(false)
         .add()
         .fieldMap("translations", "translations")
         .mapNulls(false)
         .mapNullsInReverse(false)
         .add()
         .byDefault(new DefaultFieldMapper[0])
         .register();
   }

   private void configureSecretQuestionData(MapperFactory factory) {
      factory.classMap(SecretQuestion.class, SecretQuestionData.class)
         .exclude("answer")
         .exclude("passwordHash")
         .exclude("normalization")
         .byDefault(new DefaultFieldMapper[0])
         .register();
   }

   private void configureSmsDeliveryStatus(MapperFactory factory) {
      factory.classMap(DeliveryStatus.class, SmsDeliveryStatusData.class)
         .fieldMap("", "status")
         .converter(ConverterId.LOWERCASE_ENUM_CONVERTER.getId())
         .add()
         .register();
   }

   private void configureLicense(MapperFactory factory) {
      factory.classMap(LicenseWarning.class, LicenseValidationWarningData.WarningData.class)
         .byDefault(new DefaultFieldMapper[0])
         .customize(new AdminRestBeanMappings.LicenseWarningMapper())
         .register();
      factory.classMap(License.class, LicenseData.class)
         .field("owner", "owner")
         .field("environment", "environment")
         .field("licensedModules", "licensedTags")
         .field("bundles", "bundles")
         .field("issueDate.date", "issuedOn")
         .field("validFromDate.date", "validFrom")
         .field("validUntilDate.date", "validUntil")
         .field("numberOfLicensedUsers:{getNrOfLicensedUsers}", "licensedUsers")
         .field("numberOfLicensedInstances:{getNrOfLicensedInstances}", "licensedInstances")
         .field("licensedMac48Addresses:{getLicensedMacAddrs}", "licensedMac48Addresses")
         .byDefault(new DefaultFieldMapper[0])
         .customize(new AdminRestBeanMappings.LicenseMapper())
         .register();
   }

   private void configureLog(MapperFactory factory) {
      factory.classMap(Log.class, LogsData.LogData.class).byDefault(new DefaultFieldMapper[0]).customize(new AdminRestBeanMappings.LogMapper()).register();
   }

   private static final class CrontoDeviceTokenMapper extends CustomMapper<CrontoDeviceToken, CrontoDeviceData> {
      public void mapAtoB(CrontoDeviceToken deviceToken, CrontoDeviceData tokenData, MappingContext context) {
         tokenData.setDisplayId(deviceToken.getDeviceIdForDisplay());
         tokenData.setLabel(deviceToken.getCustomName());
         tokenData.setPlatform(deviceToken.getPlatformType().<String>map(PlatformType::getIdentifier).orElse(null));
         tokenData.setActivationDate(deviceToken.getActivationDate());
         tokenData.setFirstUsage(deviceToken.getFirstUsageDate());
         tokenData.setLastUsage(deviceToken.getLatestUsageDate());
         tokenData.setNumberOfUsages(deviceToken.getTotalUsages());
         tokenData.setEnabled(deviceToken.isActive());
         tokenData.setPushNotificationsStatus(deviceToken.getPushNotificationStatus().getStatus());
      }
   }

   private static final class CrontoLetterTokenMapper extends CustomMapper<CrontoLetterTokenData, CrontoActivationCodeData> {
      public void mapAtoB(CrontoLetterTokenData letterTokenData, CrontoActivationCodeData tokenData, MappingContext context) {
         tokenData.setAllowedPlatforms(AdminRestBeanMappings.getAllowedPlatforms(letterTokenData));
         tokenData.setAllowedNumberOfActivations(letterTokenData.numberOfAllowedActivations().orElse(-1));
         tokenData.setValidFrom(letterTokenData.validityRange().getFromDate());
         tokenData.setValidTo(letterTokenData.validityRange().getToDate());
         tokenData.setGenerationDate(letterTokenData.generationDate());
         tokenData.setFirstUsage(letterTokenData.firstUsageDate());
         tokenData.setLastUsage(letterTokenData.latestUsageDate());
         tokenData.setNumberOfUsages(letterTokenData.totalUsages());
         tokenData.setPrintingOptions(letterTokenData.printingOptions().toArray(String[]::new));
         tokenData.setLastPrintingDate((Date)letterTokenData.lastPrintingDate().orElse(null));
         Optional<Cryptogram> activationCryptogram = letterTokenData.activationCryptogram();
         if (activationCryptogram.isPresent()) {
            tokenData.setConfidential(new CrontoConfidentialActivationData(activationCryptogram.get().base64image()));
         }
      }
   }

   private static final class HotpTokenMapper extends CustomMapper<HotpToken, UserHotpTokenData> {
      public void mapAtoB(HotpToken hotpToken, UserHotpTokenData tokenData, MappingContext context) {
         if (hotpToken.getSharedSecret().isPresent()) {
            Validate.isTrue(hotpToken.getSecretBase32().isPresent());
            Validate.isTrue(hotpToken.getSecretHex().isPresent());
            UserOathOtpTokenSecretData secretData = UserOathOtpTokenSecretData.builder()
               .base32((String)hotpToken.getSecretBase32().get())
               .qrCodeDataUri(OathOtpTokenQrCodeView.defaultView().dataUriOf(hotpToken).toString())
               .hex((String)hotpToken.getSecretHex().get())
               .build();
            tokenData.setSecret(secretData);
         }
      }
   }

   private static final class LicenseMapper extends CustomMapper<License, LicenseData> {
      public void mapAtoB(License license, LicenseData data, MappingContext context) {
         if (license.getBundles().isEmpty()) {
            data.setBundles(null);
         }

         if (license.getLicensedMacAddrs().isEmpty()) {
            data.setLicensedMac48Addresses(null);
         }

         if (license.getLicensedModules().isEmpty()) {
            data.setLicensedTags(null);
         }
      }
   }

   private static final class LicenseWarningMapper extends CustomMapper<LicenseWarning, LicenseValidationWarningData.WarningData> {
      public void mapAtoB(LicenseWarning licenseWarning, LicenseValidationWarningData.WarningData data, MappingContext context) {
         data.setCode(licenseWarning.getId());
         data.setSeverity(licenseWarning.getSeverity().name());
         if (licenseWarning.getArguments().isEmpty()) {
            data.setParams(null);
         } else {
            Map<String, Object> argumentsWithDatAsLocalDate = licenseWarning.getArguments()
               .entrySet()
               .stream()
               .collect(
                  Collectors.toMap(
                     Entry::getKey, entry -> entry.getValue() instanceof Date ? LocalDateHelper.toLocalDate((Date)entry.getValue()) : entry.getValue()
                  )
               );
            data.setParams(argumentsWithDatAsLocalDate);
         }
      }
   }

   private static final class LogMapper extends CustomMapper<Log, LogsData.LogData> {
      public void mapAtoB(Log log, LogsData.LogData data, MappingContext context) {
         switch (log.getSignatureVerification()) {
            case STATUS_VERIFIED:
               data.setSignatureVerification("VERIFIED");
               break;
            case STATUS_VERIFIED_NOT_IN_ORDER:
               data.setSignatureVerification("OUT_OF_ORDER");
               break;
            case STATUS_TAMPERED:
               data.setSignatureVerification("TAMPERED");
               break;
            case STATUS_WRONG_PUBLIC_KEY:
               data.setSignatureVerification("WRONG_PUBLIC_KEY");
               break;
            case STATUS_UNKNOWN:
               data.setSignatureVerification("UNKNOWN");
               break;
            case STATUS_NO_AUDITLOG_ENTRY:
               data.setSignatureVerification(null);
               break;
            default:
               throw new IllegalStateException("Cannot map signature verification for enum " + log.getSignatureVerification());
         }
      }
   }

   private static final class PersistentUserUserAuthenticationMethodMapper extends CustomMapper<PersistentUser, UserAuthenticationMethodData> {
      public void mapAtoB(PersistentUser user, UserAuthenticationMethodData authenticationMethod, MappingContext context) {
         if (!user.getMultipleNextAuthMethods().isEmpty()) {
            UserAuthenticationMethodData.UserAuthenticationMethodMigration migration = new UserAuthenticationMethodData.UserAuthenticationMethodMigration();
            migration.setNextMethods(user.getMultipleNextAuthMethods());
            migration.setUntil(user.getAuthMigrationDate());
            authenticationMethod.setMigration(migration);
         }

         List<String> additionalActiveAuthMethodKeys = (List<String>)context.getProperty("additionalActiveAuthMethodKeys");
         if (additionalActiveAuthMethodKeys != null) {
            Map<String, String> map = Maps.newHashMap();

            for (String key : additionalActiveAuthMethodKeys) {
               map.put(key, (String)user.getContextData().get(key));
            }

            authenticationMethod.setAdditionalActiveAuthMethods(map);
         }
      }
   }

   private static final class PersistentUserUserDataMapper extends CustomMapper<PersistentUser, UserData> {
      public void mapAtoB(PersistentUser user, UserData data, MappingContext context) {
         if (user.isLocked()) {
            data.setLock(new LockData(user.getLockReason(), user.getLockDate()));
         }
      }
   }

   private static final class TotpTokenMapper extends CustomMapper<TotpToken, UserTotpTokenData> {
      public void mapAtoB(TotpToken totpToken, UserTotpTokenData tokenData, MappingContext context) {
         if (totpToken.getSharedSecret().isPresent()) {
            Validate.isTrue(totpToken.getSecretBase32().isPresent());
            Validate.isTrue(totpToken.getSecretHex().isPresent());
            UserOathOtpTokenSecretData secretData = UserOathOtpTokenSecretData.builder()
               .base32((String)totpToken.getSecretBase32().get())
               .qrCodeDataUri(OathOtpTokenQrCodeView.defaultView().dataUriOf(totpToken).toString())
               .hex((String)totpToken.getSecretHex().get())
               .build();
            tokenData.setSecret(secretData);
         }
      }
   }

   private static final class UserVascoDeviceUserViewMapper extends CustomMapper<VascoOtpDeviceUserView, UserVascoDeviceData> {
      public void mapAtoB(VascoOtpDeviceUserView deviceToken, UserVascoDeviceData tokenData, MappingContext context) {
         tokenData.setEnabled(deviceToken.isEnabled());
         deviceToken.getActivationDate().ifPresent(tokenData::setActivationDate);
         deviceToken.getValidFrom().ifPresent(tokenData::setValidFrom);
         deviceToken.getValidTo().ifPresent(tokenData::setValidTo);
         deviceToken.getFirstUsageDate().ifPresent(tokenData::setFirstUsageDate);
         deviceToken.getLastUsageDate().ifPresent(tokenData::setLatestUsageDate);
         tokenData.setNumberOfUsages(deviceToken.getTotalUsages());
         deviceToken.getTrackingId().ifPresent(tokenData::setTrackingId);
         deviceToken.getComment().ifPresent(tokenData::setComment);
         deviceToken.getAdditionalInfo().ifPresent(tokenData::setAdditionalInformation);
      }
   }

   private static final class UserVascoTokenActivationLetterOrderStatusMapper
      extends CustomMapper<VascoOtpDeviceUserView, UserVascoTokenActivationLetterOrderStatusData> {
      public void mapAtoB(VascoOtpDeviceUserView a, UserVascoTokenActivationLetterOrderStatusData b, MappingContext context) {
         a.getLetterOrder().ifPresent(order -> {
            b.setOrdered(true);
            b.setOrderDate(order.getOrderDate());
            b.setOrderedBy(order.getOrderedBy());
         });
         if (a.getLetterOrder().isEmpty()) {
            b.setOrdered(false);
            b.setOrderDate(null);
            b.setOrderedBy(null);
         }
      }
   }

   private static final class VascoOtpDeviceMapping extends CustomMapper<VascoOtpDevice, VascoDeviceData> {
      public void mapAtoB(VascoOtpDevice a, VascoDeviceData b, MappingContext context) {
         b.setEnabled(a.isEnabled());
         a.getActivationDate().ifPresent(b::setActivationDate);
         a.getValidFrom().ifPresent(b::setValidFrom);
         a.getValidTo().ifPresent(b::setValidTo);
         a.getFirstUsageDate().ifPresent(b::setFirstUsageDate);
         a.getLastUsageDate().ifPresent(b::setLatestUsageDate);
         b.setNumberOfUsages(a.getTotalUsages());
      }
   }

   private static final class VascoOtpDeviceMetaDataMapping extends CustomMapper<VascoOtpDevice, ResourceObjectMetaData> {
      public void mapAtoB(VascoOtpDevice device, ResourceObjectMetaData metaData, MappingContext context) {
         this.findAssigneesRelationship(device).ifPresent(rel -> metaData.withRelationship("assignees", rel));
      }

      private Optional<Relationship> findAssigneesRelationship(VascoOtpDevice device) {
         if (device.getAssignedUsers().isEmpty()) {
            return Optional.empty();
         }

         ToManyRelationship assigneesRel = new ToManyRelationship();
         assigneesRel.setData(device.getAssignedUsers().stream().map(this::toResourceIdentifier).collect(Collectors.toList()));
         return Optional.of(assigneesRel);
      }

      private ResourceIdentifier toResourceIdentifier(VascoOtpAssignment assignment) {
         Metadata meta = new VascoAssigneeRelationshipMetadata();
         assignment.getComment().ifPresent(comment -> meta.addValue("comment", comment));
         assignment.getAdditionalInfo().ifPresent(info -> meta.addValue("additionalInformation", info));
         ResourceIdentifier identifier = new ResourceIdentifier();
         identifier.setType("user");
         identifier.setId(assignment.getUserId());
         if (!MapUtils.isEmpty(meta.getValues())) {
            identifier.setMeta(meta);
         }

         return identifier;
      }
   }

   private static final class VascoOtpDeviceUserViewMapping extends CustomMapper<VascoOtpDeviceUserView, ResourceObjectMetaData> {
      public void mapAtoB(VascoOtpDeviceUserView device, ResourceObjectMetaData metaData, MappingContext context) {
         this.findNextDeviceRelationship(device).ifPresent(rel -> metaData.withRelationship("next-device", rel));
         this.findPreviousDeviceRelationship(device).ifPresent(rel -> metaData.withRelationship("previous-device", rel));
      }

      private Optional<Relationship> findNextDeviceRelationship(VascoOtpDeviceUserView device) {
         if (device.getNextDeviceId().isEmpty()) {
            return Optional.empty();
         }

         ToOneRelationship nextDeviceRel = new ToOneRelationship();
         nextDeviceRel.setData(this.toResourceIdentifier(device.getNextDeviceId().get()));
         return Optional.of(nextDeviceRel);
      }

      private Optional<Relationship> findPreviousDeviceRelationship(VascoOtpDeviceUserView device) {
         if (device.getPreviousDeviceId().isEmpty()) {
            return Optional.empty();
         }

         ToOneRelationship prevDeviceRel = new ToOneRelationship();
         prevDeviceRel.setData(this.toResourceIdentifier(device.getPreviousDeviceId().get()));
         return Optional.of(prevDeviceRel);
      }

      private ResourceIdentifier toResourceIdentifier(VascoOtpDeviceId id) {
         ResourceIdentifier identifier = new ResourceIdentifier();
         identifier.setType("user.token.vasco.device");
         identifier.setId(id.asString());
         return identifier;
      }
   }
}
