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

import ch.ergon.restal.jsonapi.document.error.ErrorData;
import com.airlock.iam.base.api.infrastructure.exception.RestErrorCode;
import com.airlock.iam.base.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.infrastructure.error.ErrorDataFormatter;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.infrastructure.logger.LogTypes;
import com.airlock.iam.common.infrastructure.util.RestIds;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Provider;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.util.Map;
import java.util.Objects;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.commons.lang3.Strings;

public abstract class BaseExceptionMapper<T extends Exception> implements ExceptionMapper<T> {
   protected static final ELogger LOG = ELoggerFactory.getLogger(BaseExceptionMapper.class);
   private final Status status;
   private final String code;
   private final Provider<ExternalUriInfo> uriInfo;

   protected BaseExceptionMapper(Status status, RestErrorCode code, Provider<ExternalUriInfo> uriInfo) {
      this.status = status;
      this.code = code.name();
      this.uriInfo = uriInfo;
   }

   protected BaseExceptionMapper(Status status, Provider<ExternalUriInfo> uriInfo) {
      this.status = status;
      this.code = null;
      this.uriInfo = uriInfo;
   }

   protected ErrorData map(T exception, ErrorData data) {
      this.mapId(exception, data);
      this.mapStatus(exception, data);
      this.mapMetadata(exception, data);
      this.mapCode(exception, data);
      return data;
   }

   protected void mapId(T exception, ErrorData data) {
      data.setId(RestIds.generateId());
   }

   protected void mapStatus(T exception, ErrorData data) {
      data.setStatus(this.status.getStatusCode());
   }

   protected void mapCode(T exception, ErrorData data) {
      data.setCode(this.code);
   }

   protected void mapMetadata(T exception, ErrorData data) {
   }

   protected void log(T exception, ErrorData data) {
   }

   protected void logException(T exception, ErrorData data) {
      if (this.shouldLogStacktraceAsError(exception)) {
         LOG.error(LogTypes.REST_EXCEPTION_HANDLER, "Uncaught exception mapped to " + ErrorDataFormatter.formatErrorData(data), exception);
      } else {
         LOG.info(
            LogTypes.REST_EXCEPTION_HANDLER,
            String.format(
               "Answered call to %s with %s. %s",
               LogUtil.logSafeQuote(Strings.CS.prependIfMissing(((ExternalUriInfo)this.uriInfo.get()).getPath(), "/", new CharSequence[0])),
               ErrorDataFormatter.formatErrorData(data),
               LogUtil.logSafeString(exception.getMessage())
            )
         );
      }

      if (LOG.isDebugEnabled() && !this.shouldLogStacktraceAsError(exception)) {
         LOG.debug(data.toString(), exception);
      }
   }

   protected boolean shouldLogStacktraceAsError(T exception) {
      return false;
   }

   public Response toResponse(T exception) {
      ErrorData errorData = this.map(exception, new ErrorData());
      this.logException(exception, errorData);
      this.log(exception, errorData);
      ErrorResponse.ErrorResponseBuilder errorResponseBuilder = ErrorResponse.builder();
      if (errorData.getMeta() != null && errorData.getMeta().getValues() != null) {
         Map<String, Object> filteredMetaData = errorData.getMeta()
            .getValues()
            .entrySet()
            .stream()
            .filter(e -> ((String)e.getKey()).equals("nextAuthStep"))
            .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
         errorResponseBuilder.meta(filteredMetaData);
         errorData.setMeta(null);
      }

      return errorResponseBuilder.status(Status.fromStatusCode(Objects.requireNonNull(errorData.getStatus()))).error(errorData).build().toResponse();
   }

   @SuppressFBWarnings(justification = "generated code")
   protected Provider<ExternalUriInfo> getUriInfo() {
      return this.uriInfo;
   }
}
