package com.airlock.iam.authentication.domain.model.airlock2fa;

import com.airlock.iam.airlock2fa.api.domain.model.Airlock2FADeviceId;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationFlowStep;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationMethodVerifyingFlowStep;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResult;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultBuilder;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultFactory;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactorDetail;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAAuthDevice;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAFactor;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAFlowBindingToken;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FALogData;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAUser;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAUserInformation;
import com.airlock.iam.factor.domain.service.airlock2fa.Airlock2FALogHelper;
import com.airlock.iam.factor.domain.service.airlock2fa.Airlock2FAUserInformationService;
import com.airlock.iam.factor.domain.service.airlock2fa.activation.Airlock2FAActivationService;
import com.airlock.iam.factor.domain.service.airlock2fa.activation.Airlock2FAFlowBindingService;
import com.airlock.iam.factor.domain.service.airlock2fa.repository.Airlock2FAUserRepository;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.application.configuration.airlock2fa.Airlock2faRecoveryTrustedSessionBindingStepConfig;
import com.airlock.iam.flow.shared.domain.model.airlock2fa.Airlock2FACurrentDeviceIdFlowAttribute;
import com.airlock.iam.flow.shared.domain.model.airlock2fa.Airlock2FARecoveryFlowBindingSettings;
import com.airlock.iam.flow.shared.domain.model.airlock2fa.Airlock2FARecoveryFlowBindingStep;
import com.airlock.iam.flow.shared.domain.model.airlock2fa.Airlock2FARecoveryFlowBindingStepSession;
import com.airlock.iam.flow.shared.domain.service.airlock2fa.Airlock2FAAuthTokenIdService;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.List;
import java.util.Optional;

@LogicForConfig(Airlock2faRecoveryTrustedSessionBindingStepConfig.class)
public class Airlock2FARecoveryFlowBindingStepImpl implements Airlock2FARecoveryFlowBindingStep, AuthenticationMethodVerifyingFlowStep {
   private static final String FLOW_BINDING_TOKEN_ATTRIBUTE = "flowBindingToken";
   private final Provider<Airlock2FAActivationService> activationService;
   private final Provider<Airlock2FAFlowBindingService> flowBindingService;
   private final Provider<Airlock2FAUserRepository> repository;
   private final Provider<PrincipalInfo> user;
   private final Provider<Airlock2FARecoveryFlowBindingSettings> settings;
   private final Provider<AuthenticationMethod> authMethod;
   private final Provider<Airlock2FARecoveryFlowBindingStepSession> stepSession;
   private final Provider<Airlock2FAAuthTokenIdService> authTokenIdService;
   private final Provider<Airlock2FAUserInformationService> userInformationService;
   private final Provider<FlowAttributeRepository> attributes;
   private final Provider<Airlock2FALogHelper> log;

   @Inject
   public Airlock2FARecoveryFlowBindingStepImpl(
      Provider<Airlock2FAActivationService> activationService,
      Provider<Airlock2FAFlowBindingService> flowBindingService,
      Provider<Airlock2FAUserRepository> repository,
      Provider<PrincipalInfo> user,
      Provider<Airlock2FARecoveryFlowBindingSettings> settings,
      Provider<AuthenticationMethod> authMethod,
      Provider<Airlock2FARecoveryFlowBindingStepSession> stepSession,
      Provider<Airlock2FAAuthTokenIdService> authTokenIdService,
      Provider<Airlock2FAUserInformationService> userInformationService,
      Provider<FlowAttributeRepository> attributes,
      Provider<Airlock2FALogHelper> log
   ) {
      this.activationService = activationService;
      this.flowBindingService = flowBindingService;
      this.repository = repository;
      this.user = user;
      this.settings = settings;
      this.authMethod = authMethod;
      this.stepSession = stepSession;
      this.authTokenIdService = authTokenIdService;
      this.userInformationService = userInformationService;
      this.attributes = attributes;
      this.log = log;
   }

   @Override
   public AuthenticationStepResult initialize() {
      Optional<Airlock2FAUser> account = ((Airlock2FAUserRepository)this.repository.get()).findBy(this.userId());
      if (account.isEmpty()) {
         return this.stepResultFactory().fail(AuthenticationFlowStep.GeneralAuthenticationStepError.NO_VALID_TOKEN).build();
      }

      this.stepSession().setAccountId(account.get().getAccountId());
      return this.stepResultFactory()
         .stay(Airlock2FARecoveryFlowBindingStep.Airlock2FARecoveryFlowBindingStepAction.AIRLOCK_2FA_RECOVERY_DEVICES_REQUIRED)
         .build();
   }

   @Override
   public StepResult startRecovery(List<Airlock2FADeviceId> deviceIds) {
      List<Airlock2FADeviceId> devicesIntersection = this.fetchUserInfo()
         .getAuthDevices()
         .stream()
         .map(Airlock2FAAuthDevice::getId)
         .filter(deviceIds::contains)
         .toList();
      if (devicesIntersection.size() != 1) {
         return this.stepResultFactory()
            .fail(Airlock2FARecoveryFlowBindingStep.Airlock2FARecoveryFlowBindingStepError.AIRLOCK_2FA_RECOVERY_DEVICES_INVALID)
            .build();
      }

      Airlock2FADeviceId deviceId = devicesIntersection.getFirst();
      this.stepSession().setDevice(deviceId);
      ((Airlock2FALogHelper)this.log.get())
         .log("Starting recovery of Airlock 2FA devices: " + String.join(", ", deviceIds.stream().map(Airlock2FADeviceId::id).toList()));
      Optional<Airlock2FAFlowBindingToken> token = ((Airlock2FAFlowBindingService)this.flowBindingService.get())
         .tryGetRecoveryFlowBindingToken(deviceIds, ((Airlock2FARecoveryFlowBindingSettings)this.settings.get()).flowBindingTokenValidity())
         .peek();
      return token.isPresent()
         ? this.recoveryRequired().addAdditionalAttribute("flowBindingToken", token.get().token()).build()
         : this.recoveryRequired().build();
   }

   @Override
   public StepResult pollRecoveryResult() {
      if (this.stepSession().getDeviceId().isEmpty()) {
         return this.stepResultFactory().failDueToUnexpectedCall("Devices must be provided before polling.").build();
      }

      Airlock2FADeviceId deviceId = this.stepSession().getDeviceIdOrThrow();
      Airlock2FAUserInformation userInfo = this.fetchUserInfo();
      return userInfo.getAuthDevices()
         .stream()
         .filter(d -> d.getMigratedFrom().isPresent() && d.getMigratedFrom().get().equals(deviceId))
         .findAny()
         .map(migratedTo -> this.handleSuccess(migratedTo.getId(), userInfo))
         .orElse(this.recoveryRequired().build());
   }

   private Airlock2FAUserInformation fetchUserInfo() {
      return ((Airlock2FAUserInformationService)this.userInformationService.get()).getUser(this.stepSession().getAccountIdOrThrow()).orElseThrow();
   }

   private AuthenticationStepResultBuilder recoveryRequired() {
      return this.stepResultFactory().stay(Airlock2FARecoveryFlowBindingStep.Airlock2FARecoveryFlowBindingStepAction.AIRLOCK_2FA_RECOVERY_REQUIRED);
   }

   private AuthenticationStepResult handleSuccess(Airlock2FADeviceId deviceId, Airlock2FAUserInformation userInfo) {
      ((FlowAttributeRepository)this.attributes.get())
         .putOrMergeAttribute(Airlock2FACurrentDeviceIdFlowAttribute.airlock2FACurrentDeviceIdFlowAttribute(deviceId));
      ((Airlock2FAAuthTokenIdService)this.authTokenIdService.get()).handleSuccessfulUsage(Airlock2FAFactor.ONE_TOUCH, userInfo, deviceId);
      ((Airlock2FAActivationService)this.activationService.get())
         .handleSuccess(userInfo.getAccountId(), deviceId, Airlock2FALogData.airlock2FALogData(userInfo.getAccountId()).withDeviceId(deviceId));
      return this.stepResultFactory().success().build();
   }

   private Airlock2FARecoveryFlowBindingStepSession stepSession() {
      return (Airlock2FARecoveryFlowBindingStepSession)this.stepSession.get();
   }

   @Override
   public Optional<StepSession> createSession() {
      return Optional.of(new Airlock2FARecoveryFlowBindingStepSession());
   }

   public AuthenticationStepResultFactory stepResultFactory() {
      return AuthenticationStepResultFactory.forFactorDetail(KnownAuthenticationFactorDetail.CRAM_AIRLOCK_2FA_ACTIVATION);
   }

   @Override
   public String getDescription() {
      return "Airlock 2FA Recovery Trusted Session Binding Step";
   }

   private UserId userId() {
      return ((PrincipalInfo)this.user.get()).getKnownIdentity().map(UserIdentity::id).orElseThrow(() -> new FailureException("User must be known"));
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return (AuthenticationMethod)this.authMethod.get();
   }
}
