package com.airlock.iam.login.app.misc;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.core.domain.model.cronto.CrontoOtp;
import com.airlock.iam.core.domain.model.cronto.challenge.vascocronto.VascoCrontoActivation2Challenge;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.cronto.CrontoActivationSession;
import com.airlock.iam.core.misc.impl.cronto.vascocronto.VascoCrontoAccountId;
import com.airlock.iam.core.misc.impl.cronto.vascocronto.VascoCrontoHandler;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoLetterToken;
import com.airlock.iam.core.misc.util.ServletUtils;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.usertrail.UserTrailLogger;
import com.airlock.iam.login.app.misc.cronto.application.CrontoApplicationResponseFormatter;
import com.airlock.iam.login.app.misc.cronto.application.CrontoMobileApplicationFormatterFactory;
import com.airlock.iam.login.app.misc.cronto.application.DigipassResponseFormatter;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

public final class CrontoOnlineInstanceActivationServlet extends CrontoAppCommunicationServlet {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoOnlineInstanceActivationServlet.class);
   private static final String DEVICE_CODE_PARAMETER = "deviceCode";
   private static final String SERIAL_NUMBER_PARAMETER = "serialNumber";
   private static final String DEVICE_IDENTIFIER_PARAMETER = "deviceIdentifier";
   private static final String MAX_DEVICES_RESOURCE_KEY = "vasco-cronto-online-activation.maximum-devices-reached";
   private static final String INVALID_LETTER_RESOURCE_KEY = "vasco-cronto-online-activation.letter-invalid";
   private static final String INVALID_PLATFORM_RESOURCE_KEY = "vasco-cronto-online-activation.platform-not-allowed";
   private static final String INVALID_REQUEST_MESSAGE = "Invalid request";

   @Override
   protected void handleCrontoRequest(
      HttpServletRequest request, HttpServletResponse response, ConfigurationContext context, Optional<CrontoHandler> crontoHandler
   ) throws IOException {
      CrontoApplicationResponseFormatter formatter = CrontoMobileApplicationFormatterFactory.forRequest(request);
      if (crontoHandler.isEmpty()) {
         LOG.info("Discard request because there is no Cronto self-service configuration available");
         formatter.formatErrorResponse(response, "Invalid request");
      } else if (crontoHandler.get() instanceof VascoCrontoHandler vascoCrontoHandler) {
         if (formatter instanceof DigipassResponseFormatter digipassResponseFormatter) {
            if (!isOnlineActivationEnabled(request, vascoCrontoHandler)) {
               LOG.info("Received unexpected or incomplete Cronto response: the request is ignored");
               formatter.formatErrorResponse(response, "Invalid request");
            } else {
               String deviceCode = request.getParameter("deviceCode");
               String serialNumber = request.getParameter("serialNumber");
               String deviceIdentifier = request.getParameter("deviceIdentifier");
               if (deviceCode != null && serialNumber != null && deviceIdentifier != null) {
                  Optional<UserId> userId = getUserIdBySerialNumber(vascoCrontoHandler, serialNumber);
                  if (userId.isEmpty()) {
                     LOG.info("No letter found for serial number " + LogUtil.logSafeQuote(serialNumber));
                     formatter.formatErrorResponse(response, "Invalid request");
                  } else if (vascoCrontoHandler.hasReachedMaxiumNumberOfDevices(userId.get())) {
                     LOG.info("User has reached maximum number of devices.");
                     formatter.formatErrorResponse(
                        response, getTranslatedErrorMessage(request, context, "vasco-cronto-online-activation.maximum-devices-reached")
                     );
                  } else if (!vascoCrontoHandler.hasValidAndUsableLetter(userId.get())) {
                     LOG.info("User does not have a valid letter.");
                     formatter.formatErrorResponse(response, getTranslatedErrorMessage(request, context, "vasco-cronto-online-activation.letter-invalid"));
                  } else {
                     CrontoActivationSession session = CrontoActivationSession.startWithLetter(userId.get());
                     if (!isDeviceCodeValid(vascoCrontoHandler, deviceCode, session)) {
                        LOG.info("Device code invalid");
                        formatter.formatErrorResponse(response, "Invalid request");
                     } else if (!isDevicePlatformAllowed(vascoCrontoHandler, session)) {
                        LOG.info("Platform type is not allowed: " + session.getDeviceInfo().getPlatformType());
                        formatter.formatErrorResponse(
                           response, getTranslatedErrorMessage(request, context, "vasco-cronto-online-activation.platform-not-allowed")
                        );
                     } else {
                        Optional<VascoCrontoActivation2Challenge> activationChallenge2 = createActivationChallenge2(
                           vascoCrontoHandler, deviceIdentifier, session
                        );
                        if (activationChallenge2.isPresent()) {
                           digipassResponseFormatter.formatInstanceActivationResponse(
                              response, activationChallenge2.get().getSecureChannelMessage().getMessage()
                           );
                           UserTrailLogger.log(
                              userId.get().getValue(),
                              "Successfully initiated online activation for Cronto device with ID "
                                 + LogUtil.logSafeQuote(session.getDeviceInfo().getDeviceId().getDisplayId())
                                 + "."
                           );
                        } else {
                           formatter.formatErrorResponse(response, "Invalid request");
                        }
                     }
                  }
               } else {
                  LOG.info("Missing parameters.");
                  formatter.formatErrorResponse(response, "Invalid request");
               }
            }
         } else {
            LOG.info("Discard request because Digipass response formatter required.");
            formatter.formatErrorResponse(response, "Invalid request");
         }
      } else {
         LOG.info("Discard request because configured cronto handler is not a vasco cronto handler.");
         formatter.formatErrorResponse(response, "Invalid request");
      }
   }

   private static boolean isDevicePlatformAllowed(VascoCrontoHandler vascoCrontoHandler, CrontoActivationSession session) {
      try {
         Optional<CrontoLetterToken> letter = session.getLetterId().flatMap(letterId -> vascoCrontoHandler.loadLetterById(session.getUserId(), letterId));
         return vascoCrontoHandler.isPlatformAllowed(session.getDeviceInfo(), letter);
      } catch (FailureException e) {
         LOG.error(com.airlock.iam.elog.api.LogTypes.ILLEGAL_ARGUMENT, "Failed to check validity of used platform: " + e.getMessage());
         return false;
      }
   }

   private static Optional<VascoCrontoActivation2Challenge> createActivationChallenge2(
      VascoCrontoHandler vascoCrontoHandler, String deviceIdentifier, CrontoActivationSession crontoActivationSession
   ) {
      try {
         VascoCrontoActivation2Challenge activationChallenge2 = vascoCrontoHandler.createActivationChallenge2(crontoActivationSession);
         vascoCrontoHandler.addOnlineActivationToken(crontoActivationSession, activationChallenge2.getSecureChannelMessage().getMessage(), deviceIdentifier);
         return Optional.of(activationChallenge2);
      } catch (PersisterException e) {
         LOG.error(com.airlock.iam.elog.api.LogTypes.DATABASE_ACCESS, "Failed to create activation challenge 2: " + e.getMessage());
      } catch (RuntimeException e) {
         LOG.error(com.airlock.iam.elog.api.LogTypes.ILLEGAL_ARGUMENT, "Failed to create activation challenge 2: " + e.getMessage());
      }

      return Optional.empty();
   }

   private static boolean isDeviceCodeValid(CrontoHandler crontoHandler, String deviceCode, CrontoActivationSession crontoActivationSession) {
      try {
         crontoHandler.verifyActivationResponse1(crontoActivationSession, new CrontoOtp(deviceCode));
         return crontoActivationSession.isResponse1Valid();
      } catch (FailureException e) {
         LOG.error(com.airlock.iam.elog.api.LogTypes.ILLEGAL_ARGUMENT, "Failed to verify activation challenge 1: " + e.getMessage());
         return false;
      }
   }

   private static Optional<UserId> getUserIdBySerialNumber(VascoCrontoHandler crontoHandler, String serialNumber) {
      try {
         return Optional.of(UserId.userId(crontoHandler.getVascoCrontoTokenManager().findUserByAccountId(new VascoCrontoAccountId(serialNumber))));
      } catch (FailureException e) {
         LOG.error(com.airlock.iam.elog.api.LogTypes.DATABASE_ACCESS, "Failed to retrieve user: " + e.getMessage());
         return Optional.empty();
      }
   }

   private static boolean isOnlineActivationEnabled(HttpServletRequest request, VascoCrontoHandler crontoHandler) {
      if (!ServletUtils.isPOST(request)) {
         LOG.info("Discard request because POST is the only supported method, but got " + request.getMethod());
         return false;
      } else if (!crontoHandler.isOnlineActivationEnabled()) {
         LOG.info("Discard request because Cronto online activation is not enabled.");
         return false;
      } else {
         return true;
      }
   }
}
