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

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.request.RequestDocument;
import com.airlock.iam.admin.application.service.administrator.AdministratorManagementService;
import com.airlock.iam.admin.domain.model.administrator.Administrator;
import com.airlock.iam.admin.domain.model.contextdata.AdminContextDataFactory;
import com.airlock.iam.admin.infrastructure.transfer.request.administrator.AdminContextData;
import com.airlock.iam.admin.infrastructure.transfer.request.administrator.AdminCreateRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.administrator.AdministratorData;
import com.airlock.iam.admin.infrastructure.transfer.response.administrator.AdministratorLockData;
import com.airlock.iam.admin.infrastructure.transfer.response.administrator.AdministratorPasswordData;
import com.airlock.iam.admin.infrastructure.transfer.response.user.LockData;
import com.airlock.iam.base.infrastructure.mapper.jsonapi.ResourceObjectMetaData;
import com.airlock.iam.common.api.domain.model.contextdata.ContextData;
import com.airlock.iam.common.api.domain.model.locking.LockReason;
import com.airlock.iam.common.api.domain.model.role.Role;
import com.airlock.iam.common.domain.model.contextdata.ContextDataFactory;
import com.airlock.iam.common.domain.model.contextdata.DenyAllContextDataAccessController;
import com.airlock.iam.common.domain.model.password.PasswordInformation;
import com.airlock.iam.common.domain.model.user.Lockable;
import com.airlock.iam.common.domain.model.user.UserLockState;
import com.airlock.iam.common.domain.model.user.profileitems.UserProfileItems;
import com.airlock.iam.core.domain.model.user.password.PasswordChangeEnforcement;
import com.airlock.iam.core.domain.model.user.password.PasswordOrder;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import ma.glasnost.orika.CustomConverter;
import ma.glasnost.orika.CustomMapper;
import ma.glasnost.orika.DefaultFieldMapper;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.MappingContext;
import ma.glasnost.orika.converter.BidirectionalConverter;
import ma.glasnost.orika.metadata.Type;

public class AdministratorMapper {
   public AdministratorMapper(MapperFactory factory) {
      this.configure(factory);
   }

   private void configure(MapperFactory factory) {
      factory.getConverterFactory().registerConverter(new AdministratorMapper.LockableToLockDataConverter());
      factory.getConverterFactory().registerConverter(new AdministratorMapper.ContextDataConverter());
      factory.getConverterFactory().registerConverter(new AdministratorMapper.CreateAdminConverter());
      factory.getConverterFactory().registerConverter(new AdministratorMapper.UpdateContextDataConverter());
      factory.classMap(Administrator.class, AdministratorData.class)
         .field("loginStatistics:{loginStatistics}", "statistics.loginHistory")
         .field("contextData:{getVisibleContextData}", "contextData")
         .field("asLockable:{asLockable}", "lock")
         .byDefault(new DefaultFieldMapper[0])
         .register();
      factory.classMap(Administrator.class, ResourceObjectMetaData.class)
         .customize(new AdministratorMapper.AdministratorResourceObjectMetaDataMapper())
         .register();
      factory.classMap(Administrator.class, AdministratorLockData.class)
         .customize(new AdministratorMapper.AdministratorAdministratorLockDataMapper())
         .register();
      factory.classMap(PasswordOrder.class, AdministratorPasswordData.AdministratorOrderPasswordData.class)
         .field("dateOfOrder", "orderDate")
         .byDefault(new DefaultFieldMapper[0])
         .register();
      factory.classMap(PasswordChangeEnforcement.class, AdministratorPasswordData.AdministratorEnforcePasswordChangeData.class)
         .field("enforcementDate", "changeAfter")
         .byDefault(new DefaultFieldMapper[0])
         .register();
      factory.classMap(Administrator.class, AdministratorPasswordData.class)
         .field("passwordInformation:{passwordInformation}", "")
         .field("passwordInformation:{passwordInformation}.passwordOrder", "orderPassword")
         .field("passwordInformation:{passwordInformation}.passwordChangeEnforcement", "enforcePasswordChange")
         .customize(new AdministratorMapper.AdministratorAdministratorPasswordDataMapper())
         .byDefault(new DefaultFieldMapper[0])
         .register();
      factory.classMap(Administrator.class, AdminContextData.class).customize(new AdministratorMapper.AdministratorAdminContextDataMapper()).register();
   }

   private static final class AdministratorAdminContextDataMapper extends CustomMapper<Administrator, AdminContextData> {
      public void mapAtoB(Administrator admin, AdminContextData contextData, MappingContext context) {
         contextData.setValues(admin.getVisibleContextData().asKeyValueMap());
      }
   }

   private static final class AdministratorAdministratorLockDataMapper extends CustomMapper<Administrator, AdministratorLockData> {
      public void mapAtoB(Administrator admin, AdministratorLockData dto, MappingContext context) {
         UserLockState lockState = admin.asLockable().lockState();
         dto.setReason(lockState.getReason().<String>map(LockReason::getResourceKey).orElse(null));
         dto.setLockDate((Date)lockState.getLockDate().orElse(null));
      }
   }

   private static final class AdministratorAdministratorPasswordDataMapper extends CustomMapper<Administrator, AdministratorPasswordData> {
      public void mapAtoB(Administrator admin, AdministratorPasswordData dto, MappingContext context) {
         PasswordInformation info = admin.passwordInformation();
         if (info.getPasswordOrder().isPresent()) {
            Objects.requireNonNull(dto.getOrderPassword()).setOrdered(true);
         }

         if (info.getPasswordChangeEnforcement().isPresent()) {
            Objects.requireNonNull(dto.getEnforcePasswordChange())
               .setEnforceChange(((PasswordChangeEnforcement)info.getPasswordChangeEnforcement().get()).isChangeAtNextLogin());
         }
      }
   }

   private static final class AdministratorResourceObjectMetaDataMapper extends CustomMapper<Administrator, ResourceObjectMetaData> {
      public void mapAtoB(Administrator admin, ResourceObjectMetaData metaData, MappingContext context) {
         metaData.withRelationship("roles", this.getRolesRelationship(admin));
      }

      private Relationship getRolesRelationship(Administrator admin) {
         List<ResourceIdentifier> roles = admin.roles().getAll().stream().map(this::toResourceIdentifier).collect(Collectors.toList());
         ToManyRelationship rolesRelationship = new ToManyRelationship();
         rolesRelationship.setData(roles);
         return rolesRelationship;
      }

      private ResourceIdentifier toResourceIdentifier(Role role) {
         ResourceIdentifier identifier = new ResourceIdentifier();
         identifier.setType("user.role");
         identifier.setId(role.formatRole());
         return identifier;
      }
   }

   private static final class ContextDataConverter extends BidirectionalConverter<ContextData, AdminContextData> {
      private final ContextDataFactory factory = new AdminContextDataFactory(UserProfileItems.empty(), new DenyAllContextDataAccessController());

      public AdminContextData convertTo(ContextData source, Type<AdminContextData> destinationType, MappingContext mappingContext) {
         AdminContextData dto = new AdminContextData();
         source.getItems().forEach(item -> dto.addValue(item.getKey(), item.getValue()));
         return dto;
      }

      public ContextData convertFrom(AdminContextData source, Type<ContextData> destinationType, MappingContext mappingContext) {
         return this.factory.fromPersistency(source.getValues());
      }
   }

   private static final class CreateAdminConverter
      extends CustomConverter<RequestDocument<AdminCreateRequest>, AdministratorManagementService.CreateAdministrator> {
      public AdministratorManagementService.CreateAdministrator convert(
         RequestDocument<AdminCreateRequest> source,
         Type<? extends AdministratorManagementService.CreateAdministrator> destinationType,
         MappingContext mappingContext
      ) {
         AdminCreateRequest attributes = (AdminCreateRequest)source.getDataAttributes();
         Map<String, Object> contextData = attributes != null ? attributes.getContextData() : Collections.emptyMap();
         return new AdministratorManagementService.CreateAdministrator(source.getData().getId(), contextData, new String[0], null);
      }

      public boolean canConvert(Type<?> sourceType, Type<?> destinationType) {
         return RequestDocument.class.equals(sourceType.getRawType()) && destinationType.equals(this.destinationType);
      }
   }

   private static final class LockableToLockDataConverter extends CustomConverter<Lockable, LockData> {
      public LockData convert(Lockable source, Type<? extends LockData> destinationType, MappingContext mappingContext) {
         return !source.isLocked()
            ? null
            : new LockData(source.lockState().getReason().<String>map(LockReason::getResourceKey).orElse(""), (Date)source.lockState().getLockDate().get());
      }
   }

   private static final class UpdateContextDataConverter
      extends CustomConverter<RequestDocument<AdminContextData>, AdministratorManagementService.AdminContextDataUpdate> {
      public AdministratorManagementService.AdminContextDataUpdate convert(
         RequestDocument<AdminContextData> source,
         Type<? extends AdministratorManagementService.AdminContextDataUpdate> destinationType,
         MappingContext mappingContext
      ) {
         AdminContextData attributes = (AdminContextData)source.getDataAttributes();
         Map<String, Object> contextData = attributes != null ? attributes.getValues() : Collections.emptyMap();
         return new AdministratorManagementService.AdminContextDataUpdate(source.getData().getId(), contextData);
      }

      public boolean canConvert(Type<?> sourceType, Type<?> destinationType) {
         return RequestDocument.class.equals(sourceType.getRawType()) && destinationType.equals(this.destinationType);
      }
   }
}
