package com.airlock.iam.common.infrastructure.gateway;

import com.airlock.iam.base.domain.service.uri.IamUriBuilder;
import com.airlock.iam.base.domain.service.uri.UrlUtils;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.infrastructure.transfer.response.GatewaySessionData;
import com.airlock.iam.common.infrastructure.transfer.response.GatewaySessionsData;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.jasminb.jsonapi.ResourceConverter;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Nullable;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;

public class GatewayRestApi {
   private static final ELogger LOG = ELoggerFactory.getLogger(GatewayRestApi.class);
   @VisibleForTesting
   static final int QUERY_PAGE_SIZE = 50;
   private final Supplier<CloseableHttpClient> httpClientSupplier;
   private final String gatewayRestApiUri;

   public GatewayRestApi(Supplier<CloseableHttpClient> httpClientSupplier, String gatewayRestApiUri) {
      this.httpClientSupplier = httpClientSupplier;
      this.gatewayRestApiUri = gatewayRestApiUri;
   }

   public void terminateUserSessions(String userId, Predicate<String> sessionFilter) {
      try {
         CloseableHttpClient httpClient = this.httpClientSupplier.get();

         try {
            try {
               this.openAdminSession(httpClient);
               this.terminateUserSessions(httpClient, userId, sessionFilter);
            } catch (IOException e) {
               LOG.fatal(
                  LogTypes.EXTERNAL_SERVICE_ACCESS,
                  String.format(
                     "Error while terminating sessions for user %s on Gateway %s: %s",
                     LogUtil.logSafeQuote(userId),
                     LogUtil.logSafeQuote(this.gatewayRestApiUri),
                     e.getMessage()
                  )
               );
            } finally {
               this.closeAdminSession(httpClient);
            }
         } catch (Throwable var14) {
            if (httpClient != null) {
               try {
                  httpClient.close();
               } catch (Throwable var11) {
                  var14.addSuppressed(var11);
               }
            }

            throw var14;
         }

         if (httpClient != null) {
            httpClient.close();
         }
      } catch (IOException e) {
         LOG.fatal(
            LogTypes.EXTERNAL_SERVICE_ACCESS,
            String.format(
               "Failed to connect to Gateway %s to terminate sessions for user %s: %s",
               LogUtil.logSafeQuote(this.gatewayRestApiUri),
               LogUtil.logSafeQuote(userId),
               e.getMessage()
            )
         );
      }
   }

   private void terminateUserSessions(CloseableHttpClient httpClient, String userId, Predicate<String> sessionFilter) throws IOException {
      boolean hasMoreSessionsToTerminate = this.terminateMaxPageSizeSessions(httpClient, userId, sessionFilter);

      while (hasMoreSessionsToTerminate) {
         hasMoreSessionsToTerminate = this.terminateMaxPageSizeSessions(httpClient, userId, sessionFilter);
      }
   }

   private boolean terminateMaxPageSizeSessions(CloseableHttpClient httpClient, String userId, Predicate<String> sessionFilter) throws IOException {
      List<String> sessionsToTerminate = this.findSessionsToTerminate(httpClient, userId, sessionFilter);
      boolean allSessionsTerminated = this.terminateSessions(httpClient, sessionsToTerminate, userId);
      LOG.info(
         String.format(
            "Attempted to terminate %d Gateway session(s) for user %s on Gateway %s.",
            sessionsToTerminate.size(),
            LogUtil.logSafeQuote(userId),
            LogUtil.logSafeQuote(this.gatewayRestApiUri)
         )
      );
      return allSessionsTerminated && mightHaveMoreSessionsToTerminate(sessionsToTerminate);
   }

   private static boolean mightHaveMoreSessionsToTerminate(List<String> sessionsToTerminate) {
      return sessionsToTerminate.size() == 50;
   }

   private void openAdminSession(CloseableHttpClient httpClient) throws IOException {
      LOG.debug(String.format("Opening Gateway rest api session with Gateway %s", LogUtil.logSafeQuote(this.gatewayRestApiUri)));
      Integer statusCode = (Integer)httpClient.execute(this.createAdminSessionRequest(), HttpResponse::getCode);
      if (!isOkStatus(statusCode)) {
         throw new IOException(
            String.format(
               "Failed to open admin session with the Gateway %s. Response status code: %d", LogUtil.logSafeQuote(this.gatewayRestApiUri), statusCode
            )
         );
      }
   }

   private void closeAdminSession(CloseableHttpClient httpClient) {
      try {
         LOG.debug(String.format("Closing Gateway rest api session with Gateway %s", LogUtil.logSafeQuote(this.gatewayRestApiUri)));
         Integer statusCode = (Integer)httpClient.execute(this.closeAdminSessionRequest(), HttpResponse::getCode);
         if (!isOkStatus(statusCode)) {
            LOG.info(
               String.format(
                  "Failed to terminate admin session with the Gateway %s. Response status code: %d", LogUtil.logSafeQuote(this.gatewayRestApiUri), statusCode
               )
            );
         }
      } catch (IOException e) {
         LOG.info(String.format("Failed to terminate admin session with the Gateway %s: %s", LogUtil.logSafeQuote(this.gatewayRestApiUri), e.getMessage()));
      }
   }

   private List<String> findSessionsToTerminate(CloseableHttpClient httpClient, String userId, Predicate<String> sessionFilter) throws IOException {
      LOG.debug(
         String.format("Querying user sessions for user %s on Gateway %s...", LogUtil.logSafeQuote(userId), LogUtil.logSafeQuote(this.gatewayRestApiUri))
      );
      HttpGet findSessionsRequest = this.findSessionsRequest(userId);
      return (List<String>)httpClient.execute(
         findSessionsRequest, response -> extractSessionIds(response, sessionFilter, findSessionsRequest.getRequestUri(), userId)
      );
   }

   private static List<String> extractSessionIds(ClassicHttpResponse httpResponse, Predicate<String> sessionFilter, String gatewayRestApiUri, String userId) throws IOException {
      if (!isOkStatus(httpResponse.getCode())) {
         throw new IOException(
            String.format(
               "Failed to find user sessions for user %s on Gateway %s. Response status code: %d",
               LogUtil.logSafeQuote(userId),
               LogUtil.logSafeQuote(gatewayRestApiUri),
               httpResponse.getCode()
            )
         );
      }

      byte[] responseBody = EntityUtils.toByteArray(httpResponse.getEntity());
      ResourceConverter jsonApiConverter = new ResourceConverter(new Class[]{GatewaySessionsData.class});
      GatewaySessionsData gatewaySessionsData = (GatewaySessionsData)jsonApiConverter.readDocument(responseBody, GatewaySessionsData.class).get();
      return gatewaySessionsData == null
         ? List.of()
         : gatewaySessionsData.getSessions().stream().map(GatewaySessionData::getSid).filter(sessionFilter).toList();
   }

   private boolean terminateSessions(CloseableHttpClient httpClient, List<String> sessionsToTerminate, String userId) throws IOException {
      boolean succeeded = true;

      for (String sid : sessionsToTerminate) {
         succeeded &= this.terminateSession(httpClient, sid, userId);
      }

      return succeeded;
   }

   private boolean terminateSession(CloseableHttpClient httpClient, String sid, String userId) throws IOException {
      LOG.debug(String.format("Terminating session %s on Gateway %s...", LogUtil.logSafeQuote(sid), LogUtil.logSafeQuote(this.gatewayRestApiUri)));
      Result<String, String> gatewayResponse = (Result<String, String>)httpClient.execute(this.sessionDeleteRequest(sid), this::evaluateGatewayResponse);
      if (gatewayResponse.isFailure()) {
         LOG.error(
            LogTypes.EXTERNAL_SERVICE_ACCESS,
            String.format(
               "Failed to terminate session %s for user %s on Gateway %s: %s",
               LogUtil.logSafeQuote(sid),
               LogUtil.logSafeQuote(userId),
               LogUtil.logSafeQuote(this.gatewayRestApiUri),
               gatewayResponse.getFailure()
            )
         );
         return false;
      } else {
         LOG.debug(
            String.format(
               "Successfully terminated session %s on Gateway %s.", LogUtil.logSafeQuote(gatewayResponse.get()), LogUtil.logSafeQuote(this.gatewayRestApiUri)
            )
         );
         return true;
      }
   }

   private Result<String, String> evaluateGatewayResponse(ClassicHttpResponse response) throws IOException {
      return !isOkStatus(response.getCode())
         ? Result.failure(String.format("Response status code: %d", response.getCode()))
         : this.evaluateGatewayResponse(response.getEntity());
   }

   private Result<String, String> evaluateGatewayResponse(@Nullable HttpEntity entity) throws IOException {
      byte[] responseBody = entity != null ? EntityUtils.toByteArray(entity) : null;
      if (responseBody != null && responseBody.length != 0) {
         ObjectMapper mapper = new ObjectMapper();
         JsonNode jsonResponse = mapper.readTree(responseBody);
         String returnCode = jsonResponse.path("data").path("attributes").path("returnCode").asText(null);
         return !"OK".equals(returnCode)
            ? Result.failure(String.format("returnCode: %s", LogUtil.logSafeQuote(returnCode)))
            : Result.result(jsonResponse.path("data").path("attributes").path("sid").asText(""));
      } else {
         return Result.failure("Empty response body");
      }
   }

   private HttpPost createAdminSessionRequest() {
      return new HttpPost(IamUriBuilder.fromUri(this.gatewayRestApiUri).path("session/create").build());
   }

   private HttpPost closeAdminSessionRequest() {
      return new HttpPost(IamUriBuilder.fromUri(this.gatewayRestApiUri).path("session/terminate").build());
   }

   private HttpDelete sessionDeleteRequest(String sid) {
      URI uri = IamUriBuilder.fromUri(this.gatewayRestApiUri).path("/system/status/sessions/").path(sid).build();
      return new HttpDelete(uri);
   }

   private static boolean isOkStatus(int statusCode) {
      return statusCode >= 200 && statusCode < 300;
   }

   private HttpGet findSessionsRequest(String userId) {
      URI uri = IamUriBuilder.fromUri(this.gatewayRestApiUri)
         .path("/system/status/sessions/query/")
         .path(UrlUtils.urlEncodePathSegment("audittoken=" + userId))
         .path("sortattribute/audittoken")
         .path("sortmode/asc")
         .path("returnattributes/*/maxresults/50")
         .build();
      return new HttpGet(uri);
   }
}
