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

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.domain.service.uri.UrlUtils;
import com.airlock.iam.base.misc.message.MessageTemplate;
import com.airlock.iam.common.api.domain.model.cronto.CrontoMessageProvider;
import com.airlock.iam.common.api.domain.model.cronto.CrontoPushInformation;
import com.airlock.iam.common.api.domain.model.cronto.CrontoTransactionInformation;
import com.airlock.iam.common.api.domain.model.user.CurrentLocale;
import com.airlock.iam.common.domain.model.principal.UserPrincipal;
import com.airlock.iam.core.misc.cronto.CrontoChallenge;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.authen.StringResourceProvider;
import com.airlock.iam.core.misc.impl.cronto.CrontoChallengeResult;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.shared.api.domain.model.valueprovider.ValueMapProvider;
import com.airlock.iam.flow.shared.application.configuration.message.GenericCrontoMessageProviderConfigSpec;
import com.google.common.base.Splitter;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@LogicForConfig(GenericCrontoMessageProviderConfigSpec.class)
public class GenericCrontoMessageProvider implements CrontoMessageProvider {
   private static final ELogger LOG = ELoggerFactory.getLogger(GenericCrontoMessageProvider.class);
   private static final int MAX_MESSAGE_SIZE = 140;
   private final GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings settings;
   private final ValueMapProvider valueProvider;
   private final StringResourceProvider stringResourceProvider;
   private final CurrentLocale locale;
   private final MessageProviderHelper messageProvider;
   private final Provider<UserPrincipal> user;

   @Inject
   public GenericCrontoMessageProvider(
      GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings settings,
      ValueMapProvider valueProvider,
      StringResourceProvider stringResourceProvider,
      CurrentLocale locale,
      Provider<UserPrincipal> user,
      MessageProviderHelper messageProvider
   ) {
      this.settings = settings;
      this.valueProvider = valueProvider;
      this.stringResourceProvider = stringResourceProvider;
      this.locale = locale;
      this.user = user;
      this.messageProvider = messageProvider;
   }

   @Override
   public CrontoTransactionInformation generateCrontoMessage(Optional<String> pushLoginId) {
      Map<String, Object> values = this.valuesWithLoginId(pushLoginId);
      String pushMessageTemplate = this.resolveMessageTemplate(values);
      return this.createMessage(pushMessageTemplate, values);
   }

   @Override
   public CrontoPushInformation generateCrontoPushInformation(Optional<String> pushLoginId) {
      Map<String, Object> values = this.valuesWithLoginId(pushLoginId);
      String pushTitle = this.messageProvider.translateMessage(this.settings.getPushTitleResourceKey(), () -> values);
      String pushSubject = this.messageProvider.translateMessage(this.settings.getPushSubjectResourceKey(), () -> values);
      return new CrontoPushInformation(pushTitle, pushSubject);
   }

   private String resolveMessageTemplate(Map<String, Object> values) {
      String resolvedResourceKey = MessageTemplate.fromNamedParameters(this.settings.getMessageResourceKey(), values)
         .formatNonGrowableMessage()
         .orElseThrow(() -> new FailureException("Unable to resolve resource key: " + this.settings.getMessageResourceKey()));
      return this.stringResourceProvider
         .translateResourceKey(this.locale.get(), resolvedResourceKey)
         .orElseThrow(() -> new FailureException("No translation found for key " + resolvedResourceKey));
   }

   private Map<String, Object> valuesWithLoginId(Optional<String> pushLoginId) {
      Map<String, Object> values = new HashMap<>(this.valueProvider.provideValueMap());
      pushLoginId.ifPresent(id -> values.put("login-id", id));
      return values;
   }

   private CrontoTransactionInformation createMessage(String trxMessageTemplate, Map<String, Object> values) {
      EncodingMessageTemplate messageTemplate = EncodingMessageTemplate.encodingMessageTemplate(
         MessageTemplate.fromNamedParameters(trxMessageTemplate, values), UrlUtils::urlEncodeUrlParameter
      );

      for (int size = 140; size > 10; size--) {
         Optional<CrontoTransactionInformation> message = messageTemplate.formatGrowableMessage(size)
            .map(this::trimMessage)
            .map(CrontoTransactionInformation::simpleMessage)
            .filter(this::checkMessageSize);
         if (message.isPresent()) {
            return message.get();
         }
      }

      LOG.debug("Cannot create small enough message with template " + trxMessageTemplate + " and values " + values);
      throw new FailureException("Cannot create small enough message with template " + trxMessageTemplate);
   }

   private String trimMessage(String message) {
      return Stream.of(message.split("&")).map(this::trimLine).filter(Objects::nonNull).collect(Collectors.joining("&"));
   }

   @Nullable
   private String trimLine(String line) {
      if (!line.contains("=")) {
         return UrlUtils.urlEncodeUrlParameter(UrlUtils.urlDecode(line).trim());
      }

      List<String> pair = Splitter.on('=').splitToList(line);
      if (pair.size() != 2) {
         throw new FailureException("Cronto message line must contain exactly 1 equal sign: " + line);
      }

      String key = UrlUtils.urlEncodeUrlParameter(UrlUtils.urlDecode(pair.getFirst()).trim());
      String value = UrlUtils.urlEncodeUrlParameter(UrlUtils.urlDecode(pair.get(1)).trim());
      return this.settings.isOmitEmptyValues() && value.isEmpty() ? null : key + "=" + value;
   }

   private boolean checkMessageSize(CrontoTransactionInformation message) {
      try {
         CrontoChallenge challenge = this.settings
            .getCrontoHandler()
            .createImageChallenge(
               ((UserPrincipal)this.user.get()).getId(),
               Optional.of(CrontoChallengeResult.CrontoMethod.SCAN_TAN),
               message,
               new CrontoPushInformation("any", "any")
            )
            .getChallenge();
         challenge.getCryptogramForScreen();
         return true;
      } catch (RuntimeException e) {
         return false;
      }
   }

   public static final class GenericCrontoMessageProviderSettings {
      private final String messageResourceKey;
      private final String pushTitleResourceKey;
      private final String pushSubjectResourceKey;
      private final boolean omitEmptyValues;
      private final CrontoHandler crontoHandler;

      @SuppressFBWarnings(justification = "generated code")
      public static GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder builder() {
         return new GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder();
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericCrontoMessageProviderSettings(
         String messageResourceKey, String pushTitleResourceKey, String pushSubjectResourceKey, boolean omitEmptyValues, CrontoHandler crontoHandler
      ) {
         this.messageResourceKey = messageResourceKey;
         this.pushTitleResourceKey = pushTitleResourceKey;
         this.pushSubjectResourceKey = pushSubjectResourceKey;
         this.omitEmptyValues = omitEmptyValues;
         this.crontoHandler = crontoHandler;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getMessageResourceKey() {
         return this.messageResourceKey;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getPushTitleResourceKey() {
         return this.pushTitleResourceKey;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getPushSubjectResourceKey() {
         return this.pushSubjectResourceKey;
      }

      @SuppressFBWarnings(justification = "generated code")
      public boolean isOmitEmptyValues() {
         return this.omitEmptyValues;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoHandler getCrontoHandler() {
         return this.crontoHandler;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class GenericCrontoMessageProviderSettingsBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private String messageResourceKey;
         @SuppressFBWarnings(justification = "generated code")
         private String pushTitleResourceKey;
         @SuppressFBWarnings(justification = "generated code")
         private String pushSubjectResourceKey;
         @SuppressFBWarnings(justification = "generated code")
         private boolean omitEmptyValues;
         @SuppressFBWarnings(justification = "generated code")
         private CrontoHandler crontoHandler;

         @SuppressFBWarnings(justification = "generated code")
         GenericCrontoMessageProviderSettingsBuilder() {
         }

         @SuppressFBWarnings(justification = "generated code")
         public GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder messageResourceKey(
            String messageResourceKey
         ) {
            this.messageResourceKey = messageResourceKey;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder pushTitleResourceKey(
            String pushTitleResourceKey
         ) {
            this.pushTitleResourceKey = pushTitleResourceKey;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder pushSubjectResourceKey(
            String pushSubjectResourceKey
         ) {
            this.pushSubjectResourceKey = pushSubjectResourceKey;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder omitEmptyValues(
            boolean omitEmptyValues
         ) {
            this.omitEmptyValues = omitEmptyValues;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder crontoHandler(
            CrontoHandler crontoHandler
         ) {
            this.crontoHandler = crontoHandler;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings build() {
            return new GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings(
               this.messageResourceKey, this.pushTitleResourceKey, this.pushSubjectResourceKey, this.omitEmptyValues, this.crontoHandler
            );
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "GenericCrontoMessageProvider.GenericCrontoMessageProviderSettings.GenericCrontoMessageProviderSettingsBuilder(messageResourceKey="
               + this.messageResourceKey
               + ", pushTitleResourceKey="
               + this.pushTitleResourceKey
               + ", pushSubjectResourceKey="
               + this.pushSubjectResourceKey
               + ", omitEmptyValues="
               + this.omitEmptyValues
               + ", crontoHandler="
               + this.crontoHandler
               + ")";
         }
      }
   }
}
