package com.airlock.iam.login.application.service.kerberos;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletionException;
import javax.security.auth.Subject;
import org.apache.commons.lang3.ArrayUtils;
import org.ietf.jgss.GSSContext;

public final class KerberosServiceImpl implements KerberosService {
   private static final ELogger DEFAULT_LOGGER = ELoggerFactory.getLogger(KerberosServiceImpl.class);
   private static final byte[] NTLM_MESSAGE_HEADER = "NTLMSSP\u0000".getBytes(StandardCharsets.US_ASCII);
   private final Subject subject;
   private final KerberosValidateActionFactory validateActionFactory;
   private final Optional<ConfigLoggingInfo> configLoggingInfo;
   private final ELogger logger;

   public KerberosServiceImpl(Subject subject, KerberosValidateActionFactory validateActionFactory, Optional<ConfigLoggingInfo> configLoggingInfo) {
      this(subject, validateActionFactory, configLoggingInfo, DEFAULT_LOGGER);
   }

   @VisibleForTesting
   KerberosServiceImpl(Subject subject, KerberosValidateActionFactory validateActionFactory, Optional<ConfigLoggingInfo> configLoggingInfo, ELogger logger) {
      this.subject = subject;
      this.validateActionFactory = validateActionFactory;
      this.configLoggingInfo = configLoggingInfo;
      this.logger = logger;
   }

   @Override
   public KerberosValidationResult validateSpnegoData(byte[] spnegoData, Optional<GSSContext> gssContext) {
      if (isSpnegoDataOfTypeNTLM(spnegoData)) {
         this.logger.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Client sent NTLM authorization ticket instead of using Kerberos, check the client setup.");
         return KerberosValidationResult.exception();
      }

      KerberosValidateAction validateAction = this.validateActionFactory.getAction(spnegoData, gssContext);

      try {
         String kerberosUsername = Subject.callAs(this.subject, validateAction::run);
         if (kerberosUsername != null) {
            this.logger.debug("SPNEGO/Kerberos handshake succeeded for username: " + LogUtil.logSafeQuote(kerberosUsername) + this.configLogMessage());
            return KerberosValidationResult.success(kerberosUsername);
         } else {
            return validateAction.getOutputToken() != null
               ? KerberosValidationResult.unfinished(validateAction.getContext(), validateAction.getOutputToken())
               : KerberosValidationResult.failed();
         }
      } catch (CompletionException e) {
         this.logError(e.getCause());
         return KerberosValidationResult.exception();
      }
   }

   private static boolean isSpnegoDataOfTypeNTLM(byte[] data) {
      return Arrays.equals(NTLM_MESSAGE_HEADER, ArrayUtils.subarray(data, 0, NTLM_MESSAGE_HEADER.length));
   }

   private void logError(Throwable e) {
      this.logger.warn(EDefaultLogType.GENERIC_LOG_TYPE, "SPNEGO/Kerberos error occurred: " + e.getMessage() + this.configLogMessage());
   }

   private String configLogMessage() {
      return this.configLoggingInfo.<String>map(KerberosServiceImpl::configLogMessage).map(s -> ". " + s).orElse("");
   }

   private static String configLogMessage(ConfigLoggingInfo info) {
      return "Configuration: Keytab File " + LogUtil.quote(info.keytabFile().toString()) + ", Service Principal " + LogUtil.quote(info.servicePrincipal());
   }
}
