package com.airlock.iam.flow.shared.infrastructure.mapper;

import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import ch.ergon.restal.jsonapi.document.error.ErrorData;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.common.application.jsonapi.ResourceIdentifierUtils;
import com.airlock.iam.common.domain.model.session.SessionId;
import com.airlock.iam.flow.api.domain.model.flow.FlowResult;
import com.airlock.iam.flow.api.domain.model.step.GenericStepError;
import com.airlock.iam.flow.api.domain.model.step.StepResultData;
import com.airlock.iam.flow.api.domain.model.step.StepResultDataWithId;
import com.airlock.iam.flow.api.infrastructure.mapper.FlowResultMapper;
import com.airlock.iam.flow.api.infrastructure.transfer.response.FlowResultData;
import com.airlock.iam.flow.infrastructure.transfer.response.FlowResponseTransformer;
import com.airlock.iam.flow.shared.domain.service.MetadataProvider;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import ma.glasnost.orika.MapperFacade;

public final class FlowResultMapperImpl implements FlowResultMapper {
   private final MapperFacade mapper;
   private final Provider<SessionId> sessionId;
   private final FlowResponseTransformer transformer;
   private final Class<? extends FlowResultData> flowResultDataType;
   private final Provider<MetadataProvider> metadataProvider;

   @Inject
   public FlowResultMapperImpl(
      MapperFacade mapper,
      Provider<SessionId> sessionId,
      FlowResponseTransformer transformer,
      Class<? extends FlowResultData> flowResultDataType,
      Provider<MetadataProvider> metadataProvider
   ) {
      this.mapper = mapper;
      this.sessionId = sessionId;
      this.transformer = transformer;
      this.flowResultDataType = flowResultDataType;
      this.metadataProvider = metadataProvider;
   }

   @Override
   public Response toResponse(FlowResult result, ResourceObject<?>... includedResourceObjects) {
      return this.toResponse(result, Collections.emptyList(), includedResourceObjects);
   }

   @Override
   public Response toResponseWithIncludedResource(FlowResult result, Object dataToInclude, Class<?> responseDataType) {
      Object mappedDataToInclude = this.mapper.map(dataToInclude, responseDataType);
      ResourceObject<Object> objectToInclude = ResourceObject.builder(ResourceIdentifierUtils.getIdFor(result), mappedDataToInclude).build();
      return this.toResponse(result, Collections.emptyList(), objectToInclude);
   }

   @Override
   public Response toResponseWithData(FlowResult result, Class<?> responseDataType) {
      if (!result.isFailed() && result.data().isPresent()) {
         StepResultData resultData = result.<StepResultData>data().get();
         Object data = this.mapper.map(resultData, responseDataType);
         String id = this.idFor(result, resultData);
         ResourceDocument<Object> resourceObject = ResourceObject.builder(id, data).document();
         return Response.ok(resourceObject).build();
      } else {
         return this.toResponse(result, Collections.emptyList());
      }
   }

   private String idFor(FlowResult result, StepResultData data) {
      return data instanceof StepResultDataWithId stepResultDataWithId ? stepResultDataWithId.id() : ResourceIdentifierUtils.getIdFor(result);
   }

   @Override
   public Response toResponse(FlowResult result, List<ErrorData> errors, ResourceObject<?>... includedResourceObjects) {
      if (result.isFailed()) {
         return this.errorResponse(result, errors);
      }

      ResponseBuilder response = Response.ok();
      ResourceDocument<? extends FlowResultData> document = this.document(result, this.flowResultDataType);
      document.setIncluded(Arrays.asList(includedResourceObjects));
      result.metadata().forEach((key, value) -> document.getMeta().addValue(key, this.mapMeta(value)));
      ((MetadataProvider)this.metadataProvider.get()).metadata().forEach((key, value) -> document.getMeta().addValue(key, this.mapMeta(value)));
      return response.entity(document).build();
   }

   private <V> Object mapMeta(V value) {
      return this.mapper.map(value, Object.class);
   }

   private <T extends FlowResultData> ResourceDocument<T> document(FlowResult result, Class<T> responseType) {
      T attributes = (T)this.mapper.map(result, responseType);
      result.additionalAttributes().forEach((key, value) -> attributes.getAdditionalAttributes().put(key, this.mapMeta(value)));
      return ResourceObject.builder(((SessionId)this.sessionId.get()).id(), attributes).document();
   }

   private Response errorResponse(FlowResult result, List<ErrorData> errors) {
      if (result.hasInternalError()) {
         if (result.error().isPresent() && result.error().get() == GenericStepError.NOT_IMPLEMENTED) {
            return ErrorResponse.notImplemented().build().toResponse();
         }

         ErrorResponse.ErrorResponseBuilder errorResponseBuilder = ErrorResponse.internalServerError();
         result.error().ifPresent(error -> errorResponseBuilder.errorCode(error::name));
         result.metadata().forEach((key, value) -> errorResponseBuilder.meta(key, this.mapMeta(value)));
         ((MetadataProvider)this.metadataProvider.get())
            .metadata()
            .forEach((metaKey, metaValue) -> errorResponseBuilder.meta(metaKey, this.mapMeta(metaValue)));
         return errorResponseBuilder.build().toResponse();
      } else {
         ErrorResponse.ErrorResponseBuilder builder = result.nextStep().map(s -> ErrorResponse.badRequest()).orElse(ErrorResponse.forbidden());
         if (!errors.isEmpty()) {
            errors.forEach(builder::error);
         } else {
            result.error().ifPresent(error -> builder.errorCode(error::name));
         }

         result.metadata().forEach((key, value) -> builder.meta(key, this.mapMeta(value)));
         ((MetadataProvider)this.metadataProvider.get()).metadata().forEach((metaKey, metaValue) -> builder.meta(metaKey, this.mapMeta(metaValue)));
         result.additionalAttributes().forEach((key, value) -> builder.meta(key, this.mapMeta(value)));
         this.transformer.transformErrorResponse(result, builder);
         return builder.build().toResponse();
      }
   }
}
