package com.airlock.iam.login.infrastructure.service.session;

import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.session.FlowAttribute;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.shared.api.domain.model.session.UserSession;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthenticationAuditTokenFlowAttribute;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthenticationAuditTokenUserSessionFlowAttribute;
import com.airlock.iam.login.application.service.session.AuditTokenFactory;
import com.airlock.iam.login.domain.model.session.AuditToken;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import lombok.NonNull;

public final class AuthenticationAuditTokenServiceImpl implements AuthenticationAuditTokenService {
   private static final ELogger LOG = ELoggerFactory.getLogger(AuthenticationAuditTokenServiceImpl.class);
   private final Provider<AuditTokenFactory> auditTokenFactory;
   private final Provider<PrincipalInfo> principalInfo;
   private final Provider<UserSession> userSession;
   private final Provider<FlowAttributeRepository> flowAttributeRepository;

   @Inject
   public AuthenticationAuditTokenServiceImpl(
      @NonNull Provider<AuditTokenFactory> auditTokenFactory,
      @NonNull Provider<PrincipalInfo> principalInfo,
      @NonNull Provider<UserSession> userSession,
      @NonNull Provider<FlowAttributeRepository> flowAttributeRepository
   ) {
      if (auditTokenFactory == null) {
         throw new NullPointerException("auditTokenFactory is marked non-null but is null");
      }

      if (principalInfo == null) {
         throw new NullPointerException("principalInfo is marked non-null but is null");
      }

      if (userSession == null) {
         throw new NullPointerException("userSession is marked non-null but is null");
      }

      if (flowAttributeRepository == null) {
         throw new NullPointerException("flowAttributeRepository is marked non-null but is null");
      }

      this.auditTokenFactory = auditTokenFactory;
      this.principalInfo = principalInfo;
      this.userSession = userSession;
      this.flowAttributeRepository = flowAttributeRepository;
   }

   @Override
   public Optional<AuditToken> getAuditToken() {
      Optional<AuditToken> auditTokenFromUserSession = this.getAuditTokenFromUserSession();
      Optional<AuditToken> auditTokenSetDuringAuthFlow = this.getAuditTokenSetDuringAuthFlow();
      if (auditTokenFromUserSession.isPresent()) {
         auditTokenSetDuringAuthFlow.ifPresent(auditToken -> this.logIgnoringAuditTokenSetDuringAuthFlow(auditToken, auditTokenFromUserSession.get()));
         return auditTokenFromUserSession;
      } else if (auditTokenSetDuringAuthFlow.isPresent()) {
         this.addAuditTokenToUserSession(auditTokenSetDuringAuthFlow.get());
         return auditTokenSetDuringAuthFlow;
      } else {
         Optional<AuditToken> newAuditToken = this.createNewAuditToken();
         newAuditToken.ifPresent(this::addAuditTokenToUserSession);
         return newAuditToken;
      }
   }

   private Optional<AuditToken> getAuditTokenFromUserSession() {
      return this.getUserSessionAttribute(AuthenticationAuditTokenUserSessionFlowAttribute.class)
         .map(AuthenticationAuditTokenUserSessionFlowAttribute::getAuditToken)
         .map(AuditToken::new);
   }

   private Optional<AuditToken> getAuditTokenSetDuringAuthFlow() {
      return ((FlowAttributeRepository)this.flowAttributeRepository.get())
         .tryGetAttribute(AuthenticationAuditTokenFlowAttribute.class)
         .map(AuthenticationAuditTokenFlowAttribute::getAuditToken)
         .map(AuditToken::new);
   }

   private Optional<AuditToken> createNewAuditToken() {
      UserId userId = ((PrincipalInfo)this.principalInfo.get()).tryGetAuthenticatedIdentity().id();
      return ((AuditTokenFactory)this.auditTokenFactory.get()).createAuditToken(userId);
   }

   private void addAuditTokenToUserSession(AuditToken auditToken) {
      ((UserSession)this.userSession.get())
         .addUserSessionAttribute(AuthenticationAuditTokenUserSessionFlowAttribute.authenticationAuditTokenUserSessionFlowAttribute(auditToken.getValue()));
   }

   private <T extends FlowAttribute<?>> Optional<T> getUserSessionAttribute(Class<T> attributeClass) {
      return ((UserSession)this.userSession.get()).userSessionAttributes().stream().filter(attributeClass::isInstance).map(attributeClass::cast).findAny();
   }

   private void logIgnoringAuditTokenSetDuringAuthFlow(AuditToken auditTokenSetDuringAuthFlow, AuditToken auditTokenFromUserSession) {
      LOG.info(
         String.format(
            "Ignoring audit token value %s set during flow, using value %s from user session.",
            LogUtil.logSafeQuote(auditTokenSetDuringAuthFlow),
            LogUtil.logSafeQuote(auditTokenFromUserSession)
         )
      );
   }
}
