package com.airlock.iam.flow.shared.domain.service.mtan;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.phonenumber.PhoneNumber;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.shared.api.domain.model.item.info.UserDataItemInfo;
import com.airlock.iam.flow.shared.domain.model.item.mtan.MtanLabelItemDefinition;
import com.airlock.iam.flow.shared.domain.model.item.mtan.MtanNumberItemDefinition;
import com.airlock.iam.flow.shared.domain.model.item.mtan.MtanNumberItems;
import com.airlock.iam.flow.shared.domain.model.mtan.MtanTokenChangeDescription;
import com.airlock.iam.flow.shared.domain.model.mtan.SelectedMtanToken;
import com.airlock.iam.flow.shared.domain.service.user.EditableStringItemChangeService;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public final class MtanTokenChangeService implements EditableStringItemChangeService {
   private final MtanNumberItems mtanNumberItems;
   private final Provider<FlowAttributeRepository> attributes;

   @Inject
   public MtanTokenChangeService(MtanNumberItems mtanNumberItems, Provider<FlowAttributeRepository> attributes) {
      this.mtanNumberItems = mtanNumberItems;
      this.attributes = attributes;
   }

   @Override
   public boolean canHandle(String key) {
      return this.mtanNumberItems.hasNumberForKey(key) || this.mtanNumberItems.hasLabelForKey(key);
   }

   @Override
   public ValidationResult handleItemChange(String key, Optional<Object> value) {
      if (this.mtanNumberItems.hasNumberForKey(key)) {
         return this.handleNumberChange(this.mtanNumberItems.getNumberItem().orElseThrow(), value.map(Object::toString).map(StringUtils::trimToNull));
      } else if (this.mtanNumberItems.hasLabelForKey(key)) {
         return this.handleLabelChange(this.mtanNumberItems.getLabelItem().orElseThrow(), value.map(Object::toString));
      } else {
         throw new FailureException("Should not be here because service cannot handle key " + LogUtil.quote(key));
      }
   }

   private ValidationResult handleNumberChange(MtanNumberItemDefinition itemDefinition, Optional<String> value) {
      if (value.isPresent() && this.isExistingPhoneNumber(value.get())) {
         return ValidationResult.valid();
      }

      ValidationResult validationResult = itemDefinition.validate(value);
      if (!validationResult.isValid()) {
         return validationResult;
      }

      if (value.isPresent()) {
         ((FlowAttributeRepository)this.attributes.get())
            .putOrMergeAttribute(MtanTokenChangeDescription.attributeWithPhoneNumber(itemDefinition.getNormalizedPhoneNumber(value.get())));
      }

      return ValidationResult.valid();
   }

   private boolean isExistingPhoneNumber(String value) {
      Optional<String> existingPhoneNumber = this.existingPhoneNumber(this.getOrInitializeFlowAttribute());
      return existingPhoneNumber.isEmpty() ? false : Strings.CS.equals(this.normalize(value), this.normalize(existingPhoneNumber.get()));
   }

   private ValidationResult handleLabelChange(MtanLabelItemDefinition itemDefinition, Optional<String> value) {
      if (value.isPresent() && this.isExistingLabel(value.get())) {
         return ValidationResult.valid();
      }

      ValidationResult validationResult = itemDefinition.validate(value);
      if (!validationResult.isValid()) {
         return validationResult;
      }

      MtanTokenChangeDescription change = value.map(MtanTokenChangeDescription::attributeWithLabel).orElse(MtanTokenChangeDescription.removeLabel());
      ((FlowAttributeRepository)this.attributes.get()).putOrMergeAttribute(change);
      return ValidationResult.valid();
   }

   private boolean isExistingLabel(String label) {
      return this.existingLabel(this.getOrInitializeFlowAttribute()).map(currentLabel -> Strings.CS.equals(label, currentLabel)).orElse(false);
   }

   @Override
   public ValidationResult validateAllData() {
      MtanTokenChangeDescription attribute = this.getOrInitializeFlowAttribute();
      ValidationResult validationResult = ValidationResult.valid();
      this.mtanNumberItems
         .getNumberItem()
         .ifPresent(item -> validationResult.mergeWith(item.validate(attribute.getNewPhoneNumber().map(PhoneNumber::getValue))));
      if (attribute.getNewLabel().hasValue()) {
         this.mtanNumberItems.getLabelItem().ifPresent(item -> validationResult.mergeWith(item.validate(Optional.ofNullable(attribute.getNewLabel().value()))));
      }

      return validationResult;
   }

   private MtanTokenChangeDescription getOrInitializeFlowAttribute() {
      return ((FlowAttributeRepository)this.attributes.get()).tryGetAttribute(MtanTokenChangeDescription.class).orElseGet(this::initializeNewFlowAttribute);
   }

   private MtanTokenChangeDescription initializeNewFlowAttribute() {
      Optional<SelectedMtanToken> selectedToken = ((FlowAttributeRepository)this.attributes.get()).tryGetAttribute(SelectedMtanToken.class);
      return selectedToken.isPresent()
         ? this.storeNewAttribute(MtanTokenChangeDescription.attributeForSelectedToken(selectedToken.get()))
         : MtanTokenChangeDescription.emptyAttribute();
   }

   private MtanTokenChangeDescription storeNewAttribute(MtanTokenChangeDescription newAttribute) {
      ((FlowAttributeRepository)this.attributes.get()).putOrMergeAttribute(newAttribute);
      return newAttribute;
   }

   @Override
   public List<UserDataItemInfo<String>> getItemInfos() {
      List<UserDataItemInfo<String>> list = new ArrayList<>();
      MtanTokenChangeDescription currentChange = this.getOrInitializeFlowAttribute();
      this.mtanNumberItems.getNumberItem().ifPresent(item -> list.add(item.getItemInfo(this.phoneNumberForItemInfo(currentChange))));
      this.mtanNumberItems.getLabelItem().ifPresent(item -> list.add(item.getItemInfo(this.currentLabel(currentChange))));
      return list;
   }

   private Optional<String> phoneNumberForItemInfo(MtanTokenChangeDescription currentChange) {
      return currentChange.getNewPhoneNumber()
         .map(PhoneNumber::getValue)
         .or(() -> this.existingPhoneNumber(currentChange).map(this::normalize).map(this::mask));
   }

   private Optional<String> existingPhoneNumber(MtanTokenChangeDescription currentChange) {
      return currentChange.getExistingToken().map(SelectedMtanToken::getPhoneNumber).map(PhoneNumber::getValue);
   }

   private Optional<String> existingLabel(MtanTokenChangeDescription currentChange) {
      return currentChange.getExistingToken().flatMap(SelectedMtanToken::getLabel);
   }

   private String mask(String number) {
      return this.mtanNumberItems.getNumberItem().map(item -> item.maskNumber(number)).orElse(number);
   }

   private String normalize(String number) {
      return this.mtanNumberItems.getNumberItem().map(item -> item.tryNormalizingNumber(number)).orElse(number);
   }

   private Optional<String> currentLabel(MtanTokenChangeDescription currentChange) {
      return currentChange.getNewLabel().hasValue()
         ? Optional.ofNullable(currentChange.getNewLabel().value())
         : currentChange.getExistingToken().flatMap(SelectedMtanToken::getLabel);
   }
}
