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

import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import ch.ergon.restal.jsonapi.document.error.ErrorData;
import ch.ergon.restal.jsonapi.document.error.ErrorDocument;
import ch.ergon.restal.jsonapi.document.error.ErrorData.Source;
import com.airlock.iam.base.api.domain.model.validation.GenericError;
import com.airlock.iam.base.api.domain.model.validation.ValidationResultType;
import com.airlock.iam.base.api.domain.model.validation.ValidatorError;
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.infrastructure.mapper.jsonapi.JsonApiErrorSourcePointerMapper;
import com.airlock.iam.base.infrastructure.mapper.jsonapi.JsonApiResourceObjectArrayFieldMapper;
import com.airlock.iam.base.infrastructure.transfer.response.ValidationErrorMetadata;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.Strings;

public class ValidationToErrorMapper {
   protected static final ELogger LOG = ELoggerFactory.getLogger(ValidationToErrorMapper.class);
   private Optional<Status> status = Optional.empty();
   private ErrorCodeMapper errorCodeMapper = new ValidationFailedErrorCodeMapper();
   private Optional<FieldMapper> fieldToSourcePointerMapper = Optional.of(new IdentityFieldMapper());
   private Optional<String> sourceParameter = Optional.empty();
   private final Provider<ExternalUriInfo> uriInfo;

   @Inject
   public ValidationToErrorMapper(Provider<ExternalUriInfo> uriInfo) {
      this.uriInfo = uriInfo;
   }

   public ValidationToErrorMapper withSourceParameter(String parameter) {
      this.sourceParameter = Optional.of(parameter);
      this.fieldToSourcePointerMapper = Optional.empty();
      return this;
   }

   public ValidationToErrorMapper withFieldToSourcePointerMapper(FieldMapper mapper) {
      this.fieldToSourcePointerMapper = Optional.of(mapper);
      return this;
   }

   public ValidationToErrorMapper withStatus(Status httpStatus) {
      this.status = Optional.of(httpStatus);
      return this;
   }

   public ValidationToErrorMapper withJsonApiAttributesMapper() {
      this.fieldToSourcePointerMapper = Optional.of(JsonApiErrorSourcePointerMapper.jsonApiErrorSourcePointerMapper());
      return this;
   }

   public ValidationToErrorMapper withJsonApiAttributesMapper(String fieldNameAsId) {
      this.fieldToSourcePointerMapper = Optional.of(JsonApiErrorSourcePointerMapper.builder().idField(fieldNameAsId).build());
      return this;
   }

   public ValidationToErrorMapper withFieldMapper(FieldMapper fieldMapper) {
      this.fieldToSourcePointerMapper = Optional.of(fieldMapper);
      return this;
   }

   public <T> ValidationToErrorMapper withJsonApiArrayParameterMapper(List<ResourceObject<T>> requestData) {
      this.fieldToSourcePointerMapper = Optional.of(JsonApiResourceObjectArrayFieldMapper.withRequestData(requestData));
      return this;
   }

   public ErrorData[] map(List<ValidatorError> validationErrors) {
      return validationErrors.stream()
         .map(
            error -> ErrorData.builder().code(this.errorCodeMapper.map(error).name()).source(this.createSource(error.getField())).meta(withMeta(error)).build()
         )
         .toList()
         .toArray(new ErrorData[0]);
   }

   private Source createSource(String errorFieldName) {
      Source source = new Source();
      this.sourceParameter.ifPresent(source::setParameter);
      this.fieldToSourcePointerMapper.ifPresent(mapper -> source.setPointer(mapper.map(errorFieldName)));
      return source;
   }

   public ValidationToErrorMapper useErrorCode(RestErrorCode errorCode) {
      return this.withErrorCodeMapper(StaticErrorCodeMapper.of(errorCode));
   }

   public ValidationToErrorMapper withErrorCodeMapper(ErrorCodeMapper mapper) {
      this.errorCodeMapper = mapper;
      return this;
   }

   public Response mapToResponse(List<ValidatorError> validationErrors) {
      Set<ValidationResultType> resultTypes = determineResultTypes(validationErrors);
      ErrorData[] errorData = this.map(validationErrors);
      this.logErrorData(errorData);
      Status responseStatus = this.status.orElseGet(() -> inferStatusFromErrors(resultTypes));
      ErrorResponse.ErrorResponseBuilder errorResponseBuilder = ErrorResponse.builder().status(responseStatus);
      Arrays.stream(errorData).forEach(errorResponseBuilder::error);
      return errorResponseBuilder.build().toResponse();
   }

   private static Set<ValidationResultType> determineResultTypes(List<ValidatorError> validationErrors) {
      return validationErrors.stream().map(ValidatorError::getType).collect(Collectors.toSet());
   }

   public List<ErrorData> toErrorData(List<ValidatorError> validationErrors) {
      ErrorData[] errorData = this.map(validationErrors);
      this.logErrorData(errorData);
      return Arrays.asList(errorData);
   }

   private void logErrorData(ErrorData[] errorData) {
      String errorDataString = Arrays.stream(errorData).map(ErrorDataFormatter::formatErrorData).collect(Collectors.joining(", "));
      String message = String.format(
         "Call to '%s' resulted in validation failures: %s",
         Strings.CS.prependIfMissing(((ExternalUriInfo)this.uriInfo.get()).getPath(), "/", new CharSequence[0]),
         errorDataString
      );
      LOG.debug(message);
   }

   private static Status inferStatusFromErrors(Set<ValidationResultType> resultTypes) {
      return resultTypes.size() != 1 || !resultTypes.contains(GenericError.NOT_UNIQUE) && !resultTypes.contains(GenericError.CONFLICT)
         ? Status.BAD_REQUEST
         : Status.CONFLICT;
   }

   private static ValidationErrorMetadata withMeta(ValidatorError error) {
      ValidationErrorMetadata meta = new ValidationErrorMetadata();
      if (error.getType() == null) {
         return null;
      }

      meta.setDetail(error.getType().getId());
      meta.setParameters(error.getParameters().isEmpty() ? null : error.getParameters());
      return meta;
   }

   private static ValidationErrorMetadata withMeta(String detail) {
      ValidationErrorMetadata meta = new ValidationErrorMetadata();
      meta.setDetail(detail);
      return meta;
   }

   private static Source sourcePointerMapper(String pointer) {
      Source source = new Source();
      source.setPointer(pointer);
      return source;
   }

   public static ErrorDocument error(RestErrorCode code, ValidationResultType validationResultType, String pointer) {
      ErrorData[] errors = new ErrorData[]{createErrorData(code, validationResultType, pointer)};
      return new ErrorDocument(errors);
   }

   public static ErrorData createErrorData(RestErrorCode errorCode, ValidationResultType validationResultType, String sourcePointer) {
      return ErrorData.builder().code(errorCode.name()).meta(withMeta(validationResultType.getId())).source(sourcePointerMapper(sourcePointer)).build();
   }
}
