package com.airlock.iam.flow.impl.domain.model.step;

import com.airlock.iam.flow.api.domain.model.step.AdditionalAttributes;
import com.airlock.iam.flow.api.domain.model.step.InternalStepResultData;
import com.airlock.iam.flow.api.domain.model.step.StepAction;
import com.airlock.iam.flow.api.domain.model.step.StepError;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.api.domain.model.step.StepResultBuilder;
import com.airlock.iam.flow.api.domain.model.step.StepResultData;
import com.airlock.iam.flow.api.domain.model.tag.Tag;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Map.Entry;

public final class StepResultImpl implements StepResult {
   private final StepResult.StepResultType type;
   private final Optional<StepAction> nextAction;
   private final Optional<StepError> error;
   private final Set<Tag> obtainedTags;
   private final Optional<? extends StepResultData> resultData;
   private final Optional<? extends InternalStepResultData> internalResultData;
   private final Map<String, Object> metadata;
   private final AdditionalAttributes additionalAttributes;

   public StepResultImpl(
      StepResult.StepResultType type,
      Optional<StepAction> nextStep,
      Optional<StepError> error,
      Set<Tag> obtainedTags,
      Optional<? extends StepResultData> resultData,
      Optional<? extends InternalStepResultData> internalResultData,
      Map<String, Object> metadata,
      AdditionalAttributes additionalAttributes
   ) {
      Preconditions.checkArgument(
         !type.isFailure() || type == StepResult.StepResultType.INTERNAL_ERROR || error.isPresent(),
         "All failures except INTERNAL_ERROR must have an error code"
      );
      Preconditions.checkArgument(type.canContainTags() || obtainedTags.isEmpty(), String.format("Step result of type %s must not contain tags.", type));
      Preconditions.checkArgument(!type.isGotoAction() || nextStep.isEmpty(), "Results with a goto action must not contain a next step action.");
      Preconditions.checkArgument(
         additionalAttributes == null || additionalAttributes.keySet().stream().noneMatch(metadata::containsKey),
         "Metadata and additional attributes must not have overlapping keys"
      );
      this.type = type;
      this.nextAction = nextStep;
      this.error = error;
      this.obtainedTags = obtainedTags;
      this.resultData = resultData;
      this.internalResultData = internalResultData;
      this.metadata = metadata;
      this.additionalAttributes = additionalAttributes == null ? new AdditionalAttributes() : additionalAttributes;
   }

   @Override
   public StepResult.StepResultType type() {
      return this.type;
   }

   @Override
   public Optional<StepAction> nextAction() {
      return this.nextAction;
   }

   @Override
   public Optional<StepError> error() {
      return this.error;
   }

   @Override
   public boolean hasError(StepError expectedError) {
      return this.error.isPresent() && this.error.get().equals(expectedError);
   }

   @Override
   public Set<Tag> obtainedTags() {
      return Set.copyOf(this.obtainedTags);
   }

   @Override
   public <T extends StepResultData> Optional<T> resultData() {
      return (Optional<T>)this.resultData;
   }

   @Override
   public <T extends InternalStepResultData> Optional<T> internalResultData() {
      return (Optional<T>)this.internalResultData;
   }

   @Override
   public Map<String, Object> metadata() {
      return Map.copyOf(this.metadata);
   }

   @Override
   public AdditionalAttributes additionalAttributes() {
      return this.additionalAttributes;
   }

   @Override
   public StepResultBuilder toBuilder() {
      StepResultBuilder result = DefaultStepResultBuilder.defaultStepResultBuilder(this.type);
      this.nextAction.ifPresent(result::nextAction);
      this.error.ifPresent(result::error);
      result.obtainedTags(Set.copyOf(this.obtainedTags));
      this.resultData.ifPresent(result::resultData);
      this.<InternalStepResultData>internalResultData().ifPresent(result::internalResultData);
      result.metadata(Map.copyOf(this.metadata));
      result.additionalAttributes(this.additionalAttributes.copy());
      return result;
   }

   @Override
   public String toString() {
      return MoreObjects.toStringHelper("StepResult")
         .add("type", this.type.name())
         .add("next action", this.nextAction.orElse(null))
         .add("error", this.error.orElse(null))
         .add("obtained tags", this.obtainedTags.isEmpty() ? null : Joiner.on(", ").join(this.obtainedTags))
         .add("resultData", this.resultData.map(Object::getClass).orElse(null))
         .add("metadata", this.metadata)
         .add("additionalAttributes", this.additionalAttributes.toLoggable())
         .omitNullValues()
         .toString();
   }

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

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public boolean equals(Object o) {
      if (o == this) {
         return true;
      } else if (!(o instanceof StepResultImpl other)) {
         return false;
      } else {
         Object this$type = this.type;
         Object other$type = other.type;
         if (this$type == null ? other$type == null : this$type.equals(other$type)) {
            Object this$nextAction = this.nextAction;
            Object other$nextAction = other.nextAction;
            if (this$nextAction == null ? other$nextAction == null : this$nextAction.equals(other$nextAction)) {
               Object this$error = this.error;
               Object other$error = other.error;
               if (this$error == null ? other$error == null : this$error.equals(other$error)) {
                  Object this$obtainedTags = this.obtainedTags;
                  Object other$obtainedTags = other.obtainedTags;
                  if (this$obtainedTags == null ? other$obtainedTags == null : this$obtainedTags.equals(other$obtainedTags)) {
                     Object this$resultData = this.resultData;
                     Object other$resultData = other.resultData;
                     if (this$resultData == null ? other$resultData == null : this$resultData.equals(other$resultData)) {
                        Object this$internalResultData = this.internalResultData;
                        Object other$internalResultData = other.internalResultData;
                        if (this$internalResultData == null ? other$internalResultData == null : this$internalResultData.equals(other$internalResultData)) {
                           Object this$metadata = this.metadata;
                           Object other$metadata = other.metadata;
                           if (this$metadata == null ? other$metadata == null : this$metadata.equals(other$metadata)) {
                              Object this$additionalAttributes = this.additionalAttributes;
                              Object other$additionalAttributes = other.additionalAttributes;
                              return this$additionalAttributes == null
                                 ? other$additionalAttributes == null
                                 : this$additionalAttributes.equals(other$additionalAttributes);
                           } else {
                              return false;
                           }
                        } else {
                           return false;
                        }
                     } else {
                        return false;
                     }
                  } else {
                     return false;
                  }
               } else {
                  return false;
               }
            } else {
               return false;
            }
         } else {
            return false;
         }
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public int hashCode() {
      int PRIME = 59;
      int result = 1;
      Object $type = this.type;
      result = result * 59 + ($type == null ? 43 : $type.hashCode());
      Object $nextAction = this.nextAction;
      result = result * 59 + ($nextAction == null ? 43 : $nextAction.hashCode());
      Object $error = this.error;
      result = result * 59 + ($error == null ? 43 : $error.hashCode());
      Object $obtainedTags = this.obtainedTags;
      result = result * 59 + ($obtainedTags == null ? 43 : $obtainedTags.hashCode());
      Object $resultData = this.resultData;
      result = result * 59 + ($resultData == null ? 43 : $resultData.hashCode());
      Object $internalResultData = this.internalResultData;
      result = result * 59 + ($internalResultData == null ? 43 : $internalResultData.hashCode());
      Object $metadata = this.metadata;
      result = result * 59 + ($metadata == null ? 43 : $metadata.hashCode());
      Object $additionalAttributes = this.additionalAttributes;
      return result * 59 + ($additionalAttributes == null ? 43 : $additionalAttributes.hashCode());
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class StepResultImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private StepResult.StepResultType type;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<StepAction> nextStep;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<StepError> error;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<Tag> obtainedTags;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<? extends StepResultData> resultData;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<? extends InternalStepResultData> internalResultData;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> metadata$key;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<Object> metadata$value;
      @SuppressFBWarnings(justification = "generated code")
      private AdditionalAttributes additionalAttributes;

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

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder type(StepResult.StepResultType type) {
         this.type = type;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder nextStep(Optional<StepAction> nextStep) {
         this.nextStep = nextStep;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder error(Optional<StepError> error) {
         this.error = error;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder obtainedTag(Tag obtainedTag) {
         if (this.obtainedTags == null) {
            this.obtainedTags = new ArrayList<>();
         }

         this.obtainedTags.add(obtainedTag);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder obtainedTags(Collection<? extends Tag> obtainedTags) {
         if (obtainedTags == null) {
            throw new NullPointerException("obtainedTags cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder clearObtainedTags() {
         if (this.obtainedTags != null) {
            this.obtainedTags.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder resultData(Optional<? extends StepResultData> resultData) {
         this.resultData = resultData;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder internalResultData(Optional<? extends InternalStepResultData> internalResultData) {
         this.internalResultData = internalResultData;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder metadataItem(String metadataItemKey, Object metadataItemValue) {
         if (this.metadata$key == null) {
            this.metadata$key = new ArrayList<>();
            this.metadata$value = new ArrayList<>();
         }

         this.metadata$key.add(metadataItemKey);
         this.metadata$value.add(metadataItemValue);
         return this;
      }

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

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

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

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder clearMetadata() {
         if (this.metadata$key != null) {
            this.metadata$key.clear();
            this.metadata$value.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl.StepResultImplBuilder additionalAttributes(AdditionalAttributes additionalAttributes) {
         this.additionalAttributes = additionalAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StepResultImpl build() {
         Set var4 = switch (this.obtainedTags == null ? 0 : this.obtainedTags.size()) {
            case 0 -> Collections.emptySet();
            case 1 -> Collections.singleton(this.obtainedTags.get(0));
            default -> {
               Set<Tag> obtainedTags = new LinkedHashSet<>(
                  this.obtainedTags.size() < 1073741824 ? 1 + this.obtainedTags.size() + (this.obtainedTags.size() - 3) / 3 : Integer.MAX_VALUE
               );
               obtainedTags.addAll(this.obtainedTags);
               yield Collections.unmodifiableSet(obtainedTags);
            }
         };

         return new StepResultImpl(
            this.type,
            this.nextStep,
            this.error,
            var4,
            this.resultData,
            this.internalResultData,
            switch (this.metadata$key == null ? 0 : this.metadata$key.size()) {
               case 0 -> Collections.emptyMap();
               case 1 -> Collections.singletonMap(this.metadata$key.get(0), this.metadata$value.get(0));
               default -> {
                  Map<String, Object> metadata = new LinkedHashMap<>(
                     this.metadata$key.size() < 1073741824 ? 1 + this.metadata$key.size() + (this.metadata$key.size() - 3) / 3 : Integer.MAX_VALUE
                  );

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

                  yield Collections.unmodifiableMap(metadata);
               }
            },
            this.additionalAttributes
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "StepResultImpl.StepResultImplBuilder(type="
            + this.type
            + ", nextStep="
            + this.nextStep
            + ", error="
            + this.error
            + ", obtainedTags="
            + this.obtainedTags
            + ", resultData="
            + this.resultData
            + ", internalResultData="
            + this.internalResultData
            + ", metadata$key="
            + this.metadata$key
            + ", metadata$value="
            + this.metadata$value
            + ", additionalAttributes="
            + this.additionalAttributes
            + ")";
      }
   }
}
