package com.airlock.iam.flow.shared.domain.model.session;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.session.FlowSession;
import com.airlock.iam.flow.api.domain.model.tag.Tag;
import com.airlock.iam.flow.api.domain.model.tag.TagName;
import com.airlock.iam.flow.domain.model.session.FlowSessionStatus;
import com.airlock.iam.flow.domain.model.session.InternalFlowSession;
import com.airlock.iam.flow.domain.model.session.MultiFlowSession;
import com.airlock.iam.flow.shared.api.domain.model.session.UserSessionScopedFlowAttribute;
import com.airlock.iam.flow.shared.api.domain.model.session.UserSessionStatus;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;

public final class UserSessionImpl implements MultiFlowCapableUserSession {
   private static final ELogger LOG = ELoggerFactory.getLogger(UserSessionImpl.class);
   private Optional<UserIdentity> userId;
   private final Set<Tag> tags;
   private final UserSessionAttributes userSessionAttributes;
   private Optional<InternalFlowSession> currentFlowSession;
   private Optional<MultiFlowSession> multiFlowSession;
   private UserSessionStatus status;

   public UserSessionImpl() {
      this(Optional.empty(), new HashSet<>(), UserSessionAttributes.initUserSessionAttributes(), Optional.empty(), Optional.empty(), UserSessionStatus.ACTIVE);
   }

   public static UserSessionImpl restoreSession(
      @NonNull Optional<UserIdentity> userId,
      Set<Tag> tags,
      Collection<UserSessionScopedFlowAttribute<?>> attributeList,
      Optional<InternalFlowSession> flowSession,
      Optional<MultiFlowSession> multiFlowSession,
      UserSessionStatus status
   ) {
      if (userId == null) {
         throw new NullPointerException("userId is marked non-null but is null");
      } else {
         return new UserSessionImpl(userId, new HashSet<>(tags), new UserSessionAttributes(attributeList), flowSession, multiFlowSession, status);
      }
   }

   @Override
   public UserSessionStatus getStatus() {
      return this.status;
   }

   @Override
   public boolean isPristine() {
      return this.userId.isEmpty()
         && this.tags.isEmpty()
         && this.userSessionAttributes.attributes().isEmpty()
         && this.currentFlowSession.isEmpty()
         && this.multiFlowSession.isEmpty()
         && this.status == UserSessionStatus.ACTIVE;
   }

   @Override
   public Optional<MultiFlowSession> getMultiFlowSession() {
      return this.multiFlowSession;
   }

   @Override
   public void setMultiFlowSession(MultiFlowSession newMultiFlowSession) {
      this.multiFlowSession = Optional.of(newMultiFlowSession);
   }

   public static UserSessionImpl restoreUserSessionFrom(UserIdentity userIdentity, UserSessionAttributes userSessionAttributes, Set<Tag> tags) {
      UserSessionImpl session = new UserSessionImpl();
      session.userId = Optional.of(userIdentity);
      userSessionAttributes.attributes().forEach(session.userSessionAttributes::addAttribute);
      session.addTags(tags);
      return session;
   }

   @Override
   public Set<Tag> removeExpiredTags() {
      Set<Tag> expiredTags = this.tags.stream().filter(Tag::isExpired).collect(Collectors.toSet());
      this.logExpiredTags(expiredTags);
      this.tags.removeAll(expiredTags);
      return expiredTags;
   }

   @Override
   public Optional<Tag> removeTagByName(TagName tagName) {
      Optional<Tag> tagToBeRemoved = this.tags.stream().filter(tag -> tag.name().equals(tagName)).findFirst();
      tagToBeRemoved.ifPresent(this.tags::remove);
      return tagToBeRemoved;
   }

   @Override
   public boolean haveUsedInitialTagsExpired() {
      return this.currentFlowSession
         .<Boolean>map(session -> !Tag.tagNamesOf(this.tags).containsAll(session.getFlowSessionTags().getUsedInitialTagNames()))
         .orElse(false);
   }

   @Override
   public Set<Tag> getTags() {
      return Set.copyOf(this.tags);
   }

   @Override
   public void addTags(Set<Tag> newTags) {
      this.tags.addAll(newTags);
   }

   @Override
   public void setCurrentFlowSession(InternalFlowSession flowSession) {
      this.currentFlowSession = Optional.of(flowSession);
   }

   @Override
   public void clearCurrentFlowSession() {
      this.currentFlowSession = Optional.empty();
   }

   @Override
   public Optional<InternalFlowSession> getCurrentFlowSession() {
      return this.currentFlowSession;
   }

   @Override
   public void terminate() {
      this.status = UserSessionStatus.TERMINATED;
   }

   @Override
   public void clear() {
      this.destroyCurrentMultiFlow();
      this.tags.clear();
      this.userId = Optional.empty();
      this.userSessionAttributes.removeAllAttributes();
   }

   @Override
   public void setUserId(UserIdentity userIdentity) {
      if (this.userId.isPresent() && !this.userId.get().hasSameId(userIdentity)) {
         throw new ViolationException("Attempted username change on existing session: " + userIdentity.id());
      }

      this.userId = Optional.of(userIdentity);
   }

   private void logExpiredTags(Set<Tag> expiredTags) {
      if (!expiredTags.isEmpty()) {
         LOG.debug("Removing the following expired tags from the user session: " + StringUtils.join(expiredTags, ", "));
      } else {
         LOG.debug("There are no expired tags to remove.");
      }
   }

   @Override
   public Optional<UserIdentity> userId() {
      return this.userId;
   }

   @Override
   public Collection<UserSessionScopedFlowAttribute<?>> userSessionAttributes() {
      return this.userSessionAttributes.attributes();
   }

   @Override
   public <T extends UserSessionScopedFlowAttribute<?>> Optional<T> findUserSessionAttribute(Class<T> type) {
      return this.userSessionAttributes.findAttribute(type);
   }

   @Override
   public void addUserSessionAttribute(UserSessionScopedFlowAttribute<?> attribute) {
      this.userSessionAttributes.addAttribute(attribute);
   }

   @Override
   public boolean removeUserSessionAttribute(Class<? extends UserSessionScopedFlowAttribute<?>> type) {
      return this.userSessionAttributes.removeAttribute(type);
   }

   @Override
   public void removeAttributeFromUserAndFlowSession(Class<? extends UserSessionScopedFlowAttribute<?>> type) {
      this.currentFlowSession.map(FlowSession::flowAttributes).ifPresent(repo -> repo.removeAttribute(type));
      this.removeUserSessionAttribute(type);
   }

   @Override
   public boolean destroyCurrentMultiFlow() {
      boolean destroyed = false;
      this.currentFlowSession.filter(fs -> fs.getStatus().isBefore(FlowSessionStatus.TERMINATED)).ifPresent(InternalFlowSession::terminate);
      if (this.multiFlowSession.isPresent()) {
         this.multiFlowSession.get().destroy();
         destroyed = true;
      }

      return destroyed;
   }

   @SuppressFBWarnings(justification = "generated code")
   private UserSessionImpl(
      Optional<UserIdentity> userId,
      Set<Tag> tags,
      UserSessionAttributes userSessionAttributes,
      Optional<InternalFlowSession> currentFlowSession,
      Optional<MultiFlowSession> multiFlowSession,
      UserSessionStatus status
   ) {
      this.userId = userId;
      this.tags = tags;
      this.userSessionAttributes = userSessionAttributes;
      this.currentFlowSession = currentFlowSession;
      this.multiFlowSession = multiFlowSession;
      this.status = status;
   }
}
