package com.airlock.iam.admin.application.service.token.vasco;

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.domain.model.token.vasco.VascoOtpLetterOrder;
import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
import com.airlock.iam.base.api.domain.model.exception.EntityNotUniqueException;
import com.airlock.iam.base.api.domain.model.store.exception.StoreFailureException;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotFoundException;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.core.misc.impl.token.VascoToken;
import com.airlock.iam.core.misc.impl.token.VascoToken.AuthenticationMode;
import com.airlock.iam.core.misc.persistency.token.TokenAssignment;
import com.airlock.iam.core.misc.persistency.token.TokenBean;
import com.airlock.iam.core.misc.persistency.token.TokenDataProvider;
import com.airlock.iam.core.misc.persistency.token.TokenDataProvider.TokenFilter;
import com.airlock.iam.core.misc.util.DateRange;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.collect.Lists;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class VascoOtpPersistencyService {
   private static final ELogger LOG = ELoggerFactory.getLogger(VascoOtpPersistencyService.class);
   private final TokenDataProvider tokenDataProvider;

   public TokenBean loadToken(VascoOtpDeviceId tokenId) {
      try {
         List<TokenBean> token = this.tokenDataProvider
            .get(VascoToken.getAuthenticationModeFilter(AuthenticationMode.RESPONSE_ONLY).filterSerialId(tokenId.asString()));
         if (token.isEmpty()) {
            throw new EntityNotFoundException("Vasco token with ID " + tokenId + " not found");
         } else if (token.size() > 1) {
            throw new EntityNotUniqueException("Vasco token with ID " + tokenId + " not unique");
         } else {
            return token.getFirst();
         }
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   public void deleteDevice(VascoOtpDeviceId deviceId) {
      TokenBean token = this.loadToken(deviceId);
      this.removeObsoletesLinks(token);

      try {
         this.tokenDataProvider.removeToken(token);
      } catch (NotFoundException e) {
         throw new EntityNotFoundException("Couldn't find token with ID " + token.getTokenId());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   private void removeObsoletesLinks(TokenBean token) {
      try {
         for (TokenBean other : this.tokenDataProvider.get(new TokenFilter().filterObsoletesToken(token))) {
            other.setObsoletesTokenId(null);
            this.updateToken(other);
            LOG.info("Removed obsoletes link of token with ID " + other.getTokenId());
         }
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   public void assign(TokenBean token, String user, String assignedBy) {
      try {
         this.tokenDataProvider.assign(token, user, assignedBy);
      } catch (NotFoundException e) {
         throw new EntityNotFoundException("Couldn't find token with ID " + token.getTokenId());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   private void unassign(TokenBean token, String user) {
      try {
         this.tokenDataProvider.unassign(token, user);
      } catch (NotFoundException e) {
         throw new EntityNotFoundException("Couldn't find token with ID " + token.getTokenId());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   public void updateToken(TokenBean token) {
      try {
         this.tokenDataProvider.updateToken(token);
      } catch (NotFoundException e) {
         throw new EntityNotFoundException("Couldn't find token with ID " + token.getTokenId());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   public void saveDevice(VascoOtpDevice device) {
      TokenBean token = this.loadToken(device.getDeviceId());
      this.updateEnabled(device, token);
      this.updateValidity(device, token);
      this.updateToken(token);
      this.persistAssignments(device, token);
   }

   private void persistAssignments(VascoOtpDevice device, TokenBean token) {
      for (VascoOtpAssignment ass : device.getAssignedUsers()) {
         TokenAssignment tokenAss = token.getAssignmentForUser(ass.getUserId());
         if (tokenAss == null) {
            this.assign(token, ass.getUserId(), ass.getAssignedBy());
         }

         this.updateAssignmentInfo(ass, token);
      }

      Set<String> assignedUsers = device.getAssignedUsers().stream().map(VascoOtpAssignment::getUserId).collect(Collectors.toSet());

      for (TokenAssignment tokenAss : Lists.newArrayList(token.getAssignments())) {
         if (!assignedUsers.contains(tokenAss.getUser())) {
            this.unassign(token, tokenAss.getUser());
         }
      }
   }

   private void updateAssignmentInfo(VascoOtpAssignment ass, TokenBean token) {
      TokenAssignment tokenAssignment = token.getAssignmentForUser(ass.getUserId());
      if (ass.hasAdditionalInfo() || ass.hasComment()) {
         ass.getAdditionalInfo().ifPresent(tokenAssignment::setAdditionalInformation);
         ass.getComment().ifPresent(tokenAssignment::setComment);
         this.updateToken(token);
      }
   }

   private void updateValidity(VascoOtpDevice device, TokenBean token) {
      token.setValidityRange(new DateRange(device.getValidFrom().orElse(null), device.getValidTo().orElse(null)));
   }

   private void updateEnabled(VascoOtpDevice device, TokenBean token) {
      if (device.isEnabled() && !token.isActive()) {
         token.setActive(true);
         if (token.getActivationDate() == null) {
            token.setActivationDate(new Date());
         }
      } else if (!device.isEnabled() && token.isActive()) {
         token.setActive(false);
      }
   }

   public void saveUserView(VascoOtpDeviceUserView userView) {
      this.saveDevice(userView.getDevice());
      if (userView.isAssignedTo(userView.getUserId())) {
         this.updateLetterOrder(userView);
      }

      this.updatePreviousNext(userView);
   }

   private void updateLetterOrder(VascoOtpDeviceUserView userView) {
      TokenBean token = this.loadToken(userView.getDeviceId());
      Optional<VascoOtpLetterOrder> letterOrder = userView.getLetterOrder();
      TokenAssignment tokenAss = token.getAssignmentForUser(userView.getUserId());
      if (letterOrder.isPresent() && !tokenAss.isOrderNew()) {
         tokenAss.orderNew(letterOrder.get().getOrderedBy(), letterOrder.get().getOrderDate());
         this.updateToken(token);
      } else if (!letterOrder.isPresent() && tokenAss.isOrderNew()) {
         tokenAss.cancelOrderNew();
         this.updateToken(token);
      }
   }

   private Optional<VascoOtpDeviceId> deviceId(Optional<TokenBean> token) {
      return token.<String>map(TokenBean::getSerialId).map(VascoOtpDeviceId::new);
   }

   private void updatePreviousNext(VascoOtpDeviceUserView userView) {
      TokenBean token = this.loadToken(userView.getDeviceId());
      Optional<TokenBean> oldPrevious = this.loadPreviousToken(userView.getUserId(), token);
      if (!userView.getPreviousDeviceId().equals(this.deviceId(oldPrevious))) {
         Optional<TokenBean> newPrevious = userView.getPreviousDeviceId().map(this::loadToken);
         token.setObsoletesTokenId(newPrevious.<String>map(TokenBean::getTokenId).orElse(null));
         this.updateToken(token);
      }

      Optional<TokenBean> oldNext = this.loadNextToken(userView.getUserId(), token);
      if (!userView.getNextDeviceId().equals(this.deviceId(oldNext))) {
         if (oldNext.isPresent()) {
            oldNext.get().setObsoletesTokenId(null);
            this.updateToken(oldNext.get());
         }

         if (userView.getNextDeviceId().isPresent()) {
            TokenBean newNext = this.loadToken(userView.getNextDeviceId().get());
            newNext.setObsoletesTokenId(token.getTokenId());
            this.updateToken(newNext);
         }
      }
   }

   private Optional<TokenBean> loadNextToken(String userId, TokenBean token) {
      try {
         return this.tokenDataProvider
            .get(
               VascoToken.getAuthenticationModeFilter(AuthenticationMode.RESPONSE_ONLY).filterObsoletesToken(token).filterUserForMultiassignmentTokens(userId)
            )
            .stream()
            .findFirst();
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   private Optional<TokenBean> loadPreviousToken(String userId, TokenBean token) {
      if (token.getObsoletesTokenId() == null) {
         return Optional.empty();
      }

      try {
         TokenBean previous = this.tokenDataProvider.getToken(token.getObsoletesTokenId());
         return previous != null && previous.getAssignmentForUser(userId) != null ? Optional.of(previous) : Optional.empty();
      } catch (NotFoundException e) {
         LOG.debug("Failed to load successive token " + token.getObsoletesTokenId() + " for token " + token.getTokenId());
         return Optional.empty();
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public VascoOtpPersistencyService(TokenDataProvider tokenDataProvider) {
      this.tokenDataProvider = tokenDataProvider;
   }
}
