package com.airlock.iam.base.infrastructure.error;

import ch.ergon.restal.jsonapi.document.DocumentMetadata;
import ch.ergon.restal.jsonapi.document.error.ErrorData;
import ch.ergon.restal.jsonapi.document.error.ErrorDocument;
import ch.ergon.restal.jsonapi.document.error.ErrorData.ErrorDataBuilder;
import com.airlock.iam.base.api.infrastructure.exception.RestErrorCode;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.security.util.SeededSecureRandom;
import com.google.common.base.Strings;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.Map.Entry;
import java.util.stream.Collectors;

@SuppressFBWarnings("UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
public final class ErrorResponse {
   private static final Random RANDOM_GENERATOR = SeededSecureRandom.getInstance();
   private static final ELogger LOG = ELoggerFactory.getLogger(ErrorResponse.class);
   @Nullable
   private final String log;
   @Nullable
   private String errorId;
   @Nullable
   private final RestErrorCode errorCode;
   private final List<ErrorData> errors;
   private final Map<String, Object> meta;
   private final Map<String, Object> headers;
   private final Status status;

   public static ErrorResponse.ErrorResponseBuilder badRequest() {
      return builder().status(Status.BAD_REQUEST);
   }

   public static ErrorResponse.ErrorResponseBuilder unauthorized() {
      return builder().status(Status.UNAUTHORIZED);
   }

   public static ErrorResponse.ErrorResponseBuilder forbidden() {
      return builder().status(Status.FORBIDDEN);
   }

   public static ErrorResponse.ErrorResponseBuilder notFound() {
      return builder().status(Status.NOT_FOUND);
   }

   public static ErrorResponse.ErrorResponseBuilder conflict() {
      return builder().status(Status.CONFLICT);
   }

   public static ErrorResponse.ErrorResponseBuilder internalServerError() {
      return builder().status(Status.INTERNAL_SERVER_ERROR);
   }

   public static ErrorResponse.ErrorResponseBuilder serviceUnavailable() {
      return builder().status(Status.SERVICE_UNAVAILABLE);
   }

   public static ErrorResponse.ErrorResponseBuilder notImplemented() {
      return builder().status(Status.NOT_IMPLEMENTED);
   }

   private ErrorDocument toErrorDocument() {
      ErrorDocument errorDocument = new ErrorDocument();
      if (this.errors.isEmpty()) {
         errorDocument.setErrors(Arrays.asList(this.simpleErrorData(this.errorCode)));
      } else {
         errorDocument.setErrors(this.errors);
      }

      DocumentMetadata metadata = Objects.requireNonNull(errorDocument.getMeta());
      this.meta.forEach(metadata::addValue);
      return errorDocument;
   }

   private ErrorData simpleErrorData(RestErrorCode code) {
      ErrorDataBuilder errorData = ErrorData.builder().status(this.status.getStatusCode());
      this.setId(errorData);
      if (code != null) {
         errorData.code(code.name());
      }

      return errorData.build();
   }

   public Response toResponse() {
      ResponseBuilder response = this.responseBuilder();
      ErrorDocument errorDocument = this.toErrorDocument();
      response.entity(errorDocument);

      for (Entry<String, Object> entry : this.headers.entrySet()) {
         response.header(entry.getKey(), entry.getValue());
      }

      this.log(errorDocument.getErrors());
      return response.build();
   }

   public void applyTo(ContainerResponseContext response) {
      ErrorDocument errorDocument = this.toErrorDocument();
      response.setEntity(errorDocument);
      response.setStatusInfo(this.status);

      for (Entry<String, Object> entry : this.headers.entrySet()) {
         response.getHeaders().add(entry.getKey(), entry.getValue());
      }

      this.log(errorDocument.getErrors());
   }

   private void setId(ErrorDataBuilder errorData) {
      if (this.errorId == null) {
         this.errorId = this.generateId();
      }

      errorData.id(this.errorId);
   }

   private String generateId() {
      long time = System.currentTimeMillis() % 10000L;
      long random = Math.abs(RANDOM_GENERATOR.nextInt() % 10000);
      return String.format("%04d:%04d", time, random);
   }

   private ResponseBuilder responseBuilder() {
      return Response.status(this.status);
   }

   private void log(List<ErrorData> errorData) {
      getErrors(errorData).ifPresent(errorString -> LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, String.format("Errors: %s", errorString)));
      if (this.log != null) {
         LOG.info(this.log);
      }
   }

   private static Optional<String> getErrors(List<ErrorData> errorDatas) {
      String errorIds = errorDatas.stream()
         .filter(ErrorDatas::is5xxStatus)
         .filter(errorData -> !Strings.isNullOrEmpty(errorData.getId()))
         .map(ErrorDatas::asString)
         .flatMap(Optional::stream)
         .collect(Collectors.joining(", "));
      return errorIds.isBlank() ? Optional.empty() : Optional.of(errorIds);
   }

   @SuppressFBWarnings(justification = "generated code")
   public static ErrorResponse.ErrorResponseBuilder builder() {
      return new ErrorResponse.ErrorResponseBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public ErrorResponse(
      @Nullable String log,
      @Nullable String errorId,
      @Nullable RestErrorCode errorCode,
      List<ErrorData> errors,
      Map<String, Object> meta,
      Map<String, Object> headers,
      Status status
   ) {
      this.log = log;
      this.errorId = errorId;
      this.errorCode = errorCode;
      this.errors = errors;
      this.meta = meta;
      this.headers = headers;
      this.status = status;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class ErrorResponseBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String log;
      @SuppressFBWarnings(justification = "generated code")
      private String errorId;
      @SuppressFBWarnings(justification = "generated code")
      private RestErrorCode errorCode;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<ErrorData> errors;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> meta$key;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<Object> meta$value;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> headers$key;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<Object> headers$value;
      @SuppressFBWarnings(justification = "generated code")
      private Status status;

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

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder log(@Nullable String log) {
         this.log = log;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder errorId(@Nullable String errorId) {
         this.errorId = errorId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder errorCode(@Nullable RestErrorCode errorCode) {
         this.errorCode = errorCode;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder error(ErrorData error) {
         if (this.errors == null) {
            this.errors = new ArrayList<>();
         }

         this.errors.add(error);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder errors(Collection<? extends ErrorData> errors) {
         if (errors == null) {
            throw new NullPointerException("errors cannot be null");
         }

         if (this.errors == null) {
            this.errors = new ArrayList<>();
         }

         this.errors.addAll(errors);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder clearErrors() {
         if (this.errors != null) {
            this.errors.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder meta(String metaKey, Object metaValue) {
         if (this.meta$key == null) {
            this.meta$key = new ArrayList<>();
            this.meta$value = new ArrayList<>();
         }

         this.meta$key.add(metaKey);
         this.meta$value.add(metaValue);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder meta(Map<? extends String, ? extends Object> meta) {
         if (meta == null) {
            throw new NullPointerException("meta cannot be null");
         }

         if (this.meta$key == null) {
            this.meta$key = new ArrayList<>();
            this.meta$value = new ArrayList<>();
         }

         for (Entry<? extends String, ? extends Object> $lombokEntry : meta.entrySet()) {
            this.meta$key.add($lombokEntry.getKey());
            this.meta$value.add($lombokEntry.getValue());
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder clearMeta() {
         if (this.meta$key != null) {
            this.meta$key.clear();
            this.meta$value.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder header(String headerKey, Object headerValue) {
         if (this.headers$key == null) {
            this.headers$key = new ArrayList<>();
            this.headers$value = new ArrayList<>();
         }

         this.headers$key.add(headerKey);
         this.headers$value.add(headerValue);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder headers(Map<? extends String, ? extends Object> headers) {
         if (headers == null) {
            throw new NullPointerException("headers cannot be null");
         }

         if (this.headers$key == null) {
            this.headers$key = new ArrayList<>();
            this.headers$value = new ArrayList<>();
         }

         for (Entry<? extends String, ? extends Object> $lombokEntry : headers.entrySet()) {
            this.headers$key.add($lombokEntry.getKey());
            this.headers$value.add($lombokEntry.getValue());
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder clearHeaders() {
         if (this.headers$key != null) {
            this.headers$key.clear();
            this.headers$value.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse.ErrorResponseBuilder status(Status status) {
         this.status = status;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ErrorResponse build() {
         List errors = switch (this.errors == null ? 0 : this.errors.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.errors.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.errors));
         };

         Map var5 = switch (this.meta$key == null ? 0 : this.meta$key.size()) {
            case 0 -> Collections.emptyMap();
            case 1 -> Collections.singletonMap(this.meta$key.get(0), this.meta$value.get(0));
            default -> {
               Map<String, Object> meta = new LinkedHashMap<>(
                  this.meta$key.size() < 1073741824 ? 1 + this.meta$key.size() + (this.meta$key.size() - 3) / 3 : Integer.MAX_VALUE
               );

               for (int $i = 0; $i < this.meta$key.size(); $i++) {
                  meta.put(this.meta$key.get($i), this.meta$value.get($i));
               }

               yield Collections.unmodifiableMap(meta);
            }
         };

         return new ErrorResponse(
            this.log,
            this.errorId,
            this.errorCode,
            errors,
            var5,
            switch (this.headers$key == null ? 0 : this.headers$key.size()) {
               case 0 -> Collections.emptyMap();
               case 1 -> Collections.singletonMap(this.headers$key.get(0), this.headers$value.get(0));
               default -> {
                  Map<String, Object> headers = new LinkedHashMap<>(
                     this.headers$key.size() < 1073741824 ? 1 + this.headers$key.size() + (this.headers$key.size() - 3) / 3 : Integer.MAX_VALUE
                  );

                  for (int $i = 0; $i < this.headers$key.size(); $i++) {
                     headers.put(this.headers$key.get($i), this.headers$value.get($i));
                  }

                  yield Collections.unmodifiableMap(headers);
               }
            },
            this.status
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ErrorResponse.ErrorResponseBuilder(log="
            + this.log
            + ", errorId="
            + this.errorId
            + ", errorCode="
            + this.errorCode
            + ", errors="
            + this.errors
            + ", meta$key="
            + this.meta$key
            + ", meta$value="
            + this.meta$value
            + ", headers$key="
            + this.headers$key
            + ", headers$value="
            + this.headers$value
            + ", status="
            + this.status
            + ")";
      }
   }
}
