package com.airlock.iam.authentication.infrastructure.idpropagation;

import com.airlock.iam.authentication.domain.model.idpropagation.OnBehalfLoginSteps;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.core.misc.authen.LogTypes;
import com.airlock.iam.core.misc.impl.sso.onbehalflogin.AbstractSingleHTTPRequestOnBehalfLoginStep;
import com.airlock.iam.core.misc.impl.sso.onbehalflogin.CookieMapping;
import com.airlock.iam.core.misc.impl.sso.onbehalflogin.OnBehalfLoginStep;
import com.airlock.iam.core.misc.impl.sso.onbehalflogin.OnBehalfLoginStepException;
import com.airlock.iam.core.misc.sso.IdentityPropagatorException;
import com.airlock.iam.core.misc.util.httpclient.HttpClientConfig;
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.log.api.signing.SigningLogger;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.protocol.HttpContext;

public final class OnBehalfLoginStepsImpl implements OnBehalfLoginSteps {
   private static final ELogger LOG = ELoggerFactory.getLogger(OnBehalfLoginStepsImpl.class);
   private final Provider<ValueMapProvider> values;
   private final Provider<HttpClientConfig> httpClient;
   private final Provider<List<OnBehalfLoginStep>> onBehalfLoginSteps;
   private final Provider<List<CookieMapping>> cookieMappings;
   private final Provider<OnBehalfLoginStepsImpl.OnBehalfLoginSettings> settings;
   private final Provider<HttpServletResponse> response;
   private final SigningLogger signingLogger;

   @Inject
   public OnBehalfLoginStepsImpl(
      Provider<ValueMapProvider> values,
      Provider<HttpClientConfig> httpClient,
      Provider<List<OnBehalfLoginStep>> onBehalfLoginSteps,
      Provider<List<CookieMapping>> cookieMappings,
      Provider<OnBehalfLoginStepsImpl.OnBehalfLoginSettings> settings,
      Provider<HttpServletResponse> response,
      SigningLogger signingLogger
   ) {
      this.values = values;
      this.httpClient = httpClient;
      this.onBehalfLoginSteps = onBehalfLoginSteps;
      this.cookieMappings = cookieMappings;
      this.settings = settings;
      this.response = response;
      this.signingLogger = signingLogger;
   }

   @Override
   public Optional<URI> performOnBehalfLogin() {
      HttpClientContext httpContext = ((HttpClientConfig)this.httpClient.get()).createHttpContextWithCookieStore();
      this.performIdentityPropagationSteps(httpContext);
      this.writeAccessCookiesToResponse(httpContext);
      return this.determineForwardLocation(httpContext);
   }

   private void performIdentityPropagationSteps(HttpContext httpContext) {
      Map<String, Object> data = ((ValueMapProvider)this.values.get()).provideValueMap();
      CloseableHttpClient redirectingClient = this.createRedirectingHttpClient();
      CloseableHttpClient notRedirectingClient = this.createDefaultHttpClient();

      for (int stepIndex = 0; stepIndex < ((List)this.onBehalfLoginSteps.get()).size(); stepIndex++) {
         OnBehalfLoginStep step = (OnBehalfLoginStep)((List)this.onBehalfLoginSteps.get()).get(stepIndex);
         CloseableHttpClient client = this.disableAutoForwardingForStep(stepIndex) ? notRedirectingClient : redirectingClient;
         httpContext.removeAttribute(AbstractSingleHTTPRequestOnBehalfLoginStep.FORWARD_LOCATION_URI_KEY);

         try {
            step.execute(client, httpContext, data, Collections.emptyList());
         } catch (OnBehalfLoginStepException e) {
            throw new FailureException(e);
         }
      }
   }

   private void writeAccessCookiesToResponse(HttpClientContext httpContext) {
      CookieStore cookieStore = httpContext.getCookieStore();
      Map<String, Cookie> sentCookies = cookieStore.getCookies().stream().collect(Collectors.toMap(Cookie::getName, Function.identity()));
      List<String> cookieNames = new ArrayList<>();

      for (CookieMapping cookieMapping : (List)this.cookieMappings.get()) {
         Optional<jakarta.servlet.http.Cookie> targetCookie = mapCookie(cookieMapping, sentCookies);
         if (targetCookie.isPresent()) {
            ((HttpServletResponse)this.response.get()).addCookie(targetCookie.get());
            String name = targetCookie.get().getName();
            cookieNames.add(name);
         }
      }

      if (!cookieNames.isEmpty()) {
         this.signingLogger.log("Added the following backend cookies to the response for identity propagation: " + cookieNames);
      }
   }

   private static Optional<jakarta.servlet.http.Cookie> mapCookie(CookieMapping mapping, Map<String, Cookie> sentCookies) {
      try {
         return Optional.ofNullable(mapping.getMappedCookie(sentCookies));
      } catch (IdentityPropagatorException e) {
         throw new FailureException(e);
      }
   }

   private Optional<URI> determineForwardLocation(HttpContext httpContext) {
      return !((OnBehalfLoginStepsImpl.OnBehalfLoginSettings)this.settings.get()).forwardToLastRedirectLocation()
         ? Optional.empty()
         : Optional.ofNullable((URI)httpContext.getAttribute(AbstractSingleHTTPRequestOnBehalfLoginStep.FORWARD_LOCATION_URI_KEY))
            .filter(this::isForwardLocationAllowed);
   }

   private boolean isForwardLocationAllowed(URI forwardLocation) {
      if (!forwardLocation.isAbsolute()) {
         LOG.warn(LogTypes.URL_DISALLOWED_SAT, "Rejecting forward location URI, because  it is not absolute: " + LogUtil.logSafeQuote(forwardLocation));
         return false;
      }

      if (StringUtils.isNotEmpty(forwardLocation.getUserInfo())) {
         LOG.warn(LogTypes.URL_DISALLOWED_SAT, "Rejecting forward location URI, because  it contains user information.");
         LOG.debug("Rejecting forward location URI, because  it contains user information: " + LogUtil.logSafeQuote(forwardLocation));
         return false;
      }

      for (Pattern p : ((OnBehalfLoginStepsImpl.OnBehalfLoginSettings)this.settings.get()).allowedForwardLocations()) {
         if (p.matcher(forwardLocation.toString()).matches()) {
            return true;
         }
      }

      LOG.warn(
         LogTypes.URL_DISALLOWED_SAT,
         "Rejecting forward location URI, because none of the allowed forwardLocation patterns matches: " + LogUtil.logSafeQuote(forwardLocation)
      );
      return false;
   }

   private boolean disableAutoForwardingForStep(int onBehalfLoginStepIdx) {
      return ((OnBehalfLoginStepsImpl.OnBehalfLoginSettings)this.settings.get()).forwardToLastRedirectLocation()
         && onBehalfLoginStepIdx == ((List)this.onBehalfLoginSteps.get()).size() - 1;
   }

   private CloseableHttpClient createDefaultHttpClient() {
      try {
         return ((HttpClientConfig)this.httpClient.get()).createHttpClient();
      } catch (GeneralSecurityException | IOException e) {
         throw new FailureException(e);
      }
   }

   private CloseableHttpClient createRedirectingHttpClient() {
      try {
         return ((HttpClientConfig)this.httpClient.get()).getHttpClientBuilder().setRedirectsEnabled(true).build();
      } catch (GeneralSecurityException | IOException e) {
         throw new FailureException(e);
      }
   }

   public record OnBehalfLoginSettings(boolean forwardToLastRedirectLocation, List<Pattern> allowedForwardLocations) {
   }
}
