package com.airlock.iam.ssi.infrastructure.service.procivis;

import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.core.domain.model.http.HttpRequest;
import com.airlock.iam.core.domain.model.http.HttpResponse;
import com.airlock.iam.core.domain.model.http.client.HttpClient;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.ssi.application.configuration.procivis.ProcivisCoreSsiServiceConfig;
import com.airlock.iam.ssi.application.configuration.procivis.ProcivisDeskSsiServiceConfig;
import com.airlock.iam.ssi.domain.model.SsiIssuanceSession;
import com.airlock.iam.ssi.domain.model.SsiPollingStatus;
import com.airlock.iam.ssi.domain.model.SsiVerificationSession;
import com.airlock.iam.ssi.domain.model.SsiVerifiedData;
import com.airlock.iam.ssi.domain.model.procivis.Sessions;
import com.airlock.iam.ssi.domain.model.step.CredentialSchema;
import com.airlock.iam.ssi.domain.model.step.IssuerId;
import com.airlock.iam.ssi.domain.model.step.ProofRequestParameters;
import com.airlock.iam.ssi.domain.model.step.SsiAttributeSettings;
import com.airlock.iam.ssi.domain.model.step.SsiClaimSettings;
import com.airlock.iam.ssi.domain.service.SsiService;
import com.airlock.iam.ssi.domain.service.procivis.ProcivisSsiServiceSettings;
import com.airlock.iam.ssi.infrastructure.rest.ProcivisRequestFactory;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.request.CredentialClaim;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.CreateData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.CredentialData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.CredentialIssuanceData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.CredentialSchemaData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.DidItem;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.ListCredentialSchemaData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.ListDidData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.ListProofRequestData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.ProofSchemaData;
import com.airlock.iam.ssi.infrastructure.service.procivis.transfer.response.ShareProofRequestData;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.Strings;

@LogicForConfig({ProcivisDeskSsiServiceConfig.class, ProcivisCoreSsiServiceConfig.class})
public final class ProcivisSsiService implements SsiService {
   private static final Supplier<ProcivisSsiService.NoResultException> NO_RESULT = ProcivisSsiService.NoResultException::new;
   private static final int UNAUTHORIZED = 401;
   private final Provider<ProcivisSsiServiceSettings> settings;
   private final Provider<HttpClient> client;
   private final Provider<ProcivisRequestFactory> requestFactory;
   private final ELogger logger;
   private final ObjectMapper objectMapper;
   private final Provider<PrincipalInfo> user;
   private final HelpdeskLogger helpdeskLogger;

   @Inject
   public ProcivisSsiService(
      Provider<ProcivisSsiServiceSettings> settings,
      Provider<HttpClient> client,
      Provider<ProcivisRequestFactory> requestFactory,
      Provider<PrincipalInfo> user,
      HelpdeskLogger helpdeskLogger
   ) {
      this(settings, client, requestFactory, ELoggerFactory.getLogger(ProcivisSsiService.class), user, helpdeskLogger);
   }

   @VisibleForTesting
   ProcivisSsiService(
      Provider<ProcivisSsiServiceSettings> settings,
      Provider<HttpClient> client,
      Provider<ProcivisRequestFactory> requestFactory,
      ELogger logger,
      Provider<PrincipalInfo> user,
      HelpdeskLogger helpDeskLogger
   ) {
      this.settings = settings;
      this.client = client;
      this.requestFactory = requestFactory;
      this.logger = logger;
      this.objectMapper = new ObjectMapper();
      this.user = user;
      this.helpdeskLogger = helpDeskLogger;
   }

   private Supplier<Optional<? extends String>> loggingEmptySupplier(Supplier<String> errorMessage) {
      return () -> {
         this.logger.info(errorMessage.get());
         return Optional.empty();
      };
   }

   private static Entry<SsiAttributeSettings, Optional<String>> findValueForAttribute(SsiAttributeSettings attr, ListProofRequestData proofData) {
      return Map.entry(
         attr,
         Arrays.stream(proofData.getProofInputs())
            .map(ListProofRequestData.ProofInputItem::getClaims)
            .flatMap(Arrays::stream)
            .filter(receivedClaim -> Strings.CS.equals(receivedClaim.getPath(), attr.name()))
            .findFirst()
            .map(ListProofRequestData.ProofInputItem.ProofClaimData::getValue)
      );
   }

   @Override
   public Optional<SsiVerificationSession> initiateProofRequest(ProofRequestParameters parameters) {
      try {
         String proofSchemaName = parameters.proofSchema().name();
         String proofSchemaId = this.retrieveProofSchemaIdOrLogError(proofSchemaName).orElseThrow(NO_RESULT);
         String didId = this.retrieveDidIdOrLogError().orElseThrow(NO_RESULT);
         String proofRequestId = this.createProofRequestOrLogError(proofSchemaId, parameters.proofSchema().name(), didId).orElseThrow(NO_RESULT);
         String url = this.shareProofRequestOrLogError(proofRequestId, proofSchemaName).orElseThrow(NO_RESULT);
         return Optional.of(new Sessions.ProcivisVerificationSession(proofRequestId, url));
      } catch (ProcivisSsiService.NoResultException | RuntimeException e) {
         this.logger.info("Could not initiate proof request. " + Optional.ofNullable(e.getMessage()).orElse(""));
         return Optional.empty();
      }
   }

   @Override
   public Result<SsiVerifiedData, SsiPollingStatus> pollProofStatus(SsiVerificationSession session, ProofRequestParameters parameters) {
      Optional<ListProofRequestData> proofRequest = this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).retrieveProofRequestFor(session.proofRequestId(), false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get()).retrieveProofRequestFor(session.proofRequestId(), true),
         response -> this.toResponseBodyObject(response.body(), ListProofRequestData.class),
         () -> "Failed to poll status of proof request with ID %s".formatted(LogUtil.quote(session.proofRequestId()))
      );
      if (proofRequest.isEmpty()) {
         return Result.failure(SsiPollingStatus.FAILED);
      }

      return switch (proofRequest.get().getState()) {
         case CREATED, PENDING, REQUESTED -> Result.failure(SsiPollingStatus.PENDING);
         case ERROR, REJECTED -> {
            this.logger.error(LogTypes.WEB_SERVICE, "Proof request failed with status: %s".formatted(proofRequest.get().getState()));
            yield Result.failure(SsiPollingStatus.FAILED);
         }
         case ACCEPTED -> this.getSsiVerifiedData(parameters, proofRequest.get());
      };
   }

   @Override
   public Optional<SsiIssuanceSession> issueCredential(CredentialSchema credentialSchema, Map<String, String> credentialAttributes) {
      try {
         String credentialSchemaName = credentialSchema.name();
         String credentialSchemaId = this.retrieveCredentialSchemaIdOrLogError(credentialSchemaName).orElseThrow(NO_RESULT);
         CredentialSchemaData credentialSchemaData = this.retrieveCredentialSchemaDataOrLogError(credentialSchemaId, credentialSchema).orElseThrow(NO_RESULT);
         List<CredentialClaim> claims = this.extractCredentialClaims(credentialSchemaData, credentialAttributes).orElseThrow(NO_RESULT);
         String didId = this.retrieveDidIdOrLogError().orElseThrow(NO_RESULT);
         String credentialId = this.createCredentialOrLogError(credentialSchemaId, didId, claims, credentialSchema).orElseThrow(NO_RESULT);
         return this.issueCredential(credentialId, credentialSchemaName).map(urlForQrCode -> new Sessions.ProcivisIssuanceSession(credentialId, urlForQrCode));
      } catch (ProcivisSsiService.NoResultException | RuntimeException e) {
         this.logger.info("Could not issue credential. " + Optional.ofNullable(e.getMessage()).orElse(""));
         return Optional.empty();
      }
   }

   @Override
   public SsiPollingStatus pollIssuanceStatus(SsiIssuanceSession session) {
      String errorMessage = "Failed to poll status of credential issuance with ID %s".formatted(LogUtil.quote(session.credentialId()));
      Optional<CredentialData> credential = this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).retrieveCredentialRequestFor(session.credentialId(), false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get()).retrieveCredentialRequestFor(session.credentialId(), true),
         response -> this.toResponseBodyObject(response.body(), CredentialData.class),
         () -> errorMessage
      );
      if (credential.isEmpty()) {
         return SsiPollingStatus.FAILED;
      }

      return switch (credential.get().getState()) {
         case CREATED, PENDING, OFFERED -> SsiPollingStatus.PENDING;
         case ERROR, REJECTED, SUSPENDED, REVOKED -> {
            String message = "Credential issuance failed with status: %s".formatted(credential.get().getState());
            this.logger.error(LogTypes.WEB_SERVICE, message);
            this.logIfUserPresent(message);
            yield SsiPollingStatus.FAILED;
         }
         case ACCEPTED -> SsiPollingStatus.SUCCESS;
      };
   }

   private Optional<String> retrieveProofSchemaIdOrLogError(String proofSchemaName) {
      Supplier<String> errorMessage = () -> "Failed to retrieve proof schema for schema name %s".formatted(LogUtil.quote(proofSchemaName));
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).listProofSchemasRequestFor(proofSchemaName, false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get()).listProofSchemasRequestFor(proofSchemaName, true),
         response -> this.extractProofSchemaId(response, errorMessage, proofSchemaName),
         errorMessage
      );
   }

   private Optional<String> extractProofSchemaId(HttpResponse response, Supplier<String> errorMessage, String proofSchemaName) {
      return this.toResponseBodyObject(response.body(), ProofSchemaData.class)
         .map(ProofSchemaData::getValues)
         .stream()
         .flatMap(Arrays::stream)
         .filter(data -> proofSchemaName.equals(data.getName()))
         .findFirst()
         .map(ProofSchemaData.ProofSchemaValueData::getId)
         .or(this.loggingEmptySupplier(errorMessage));
   }

   private Optional<String> retrieveDidIdOrLogError() {
      Supplier<String> errorMessage = () -> "Failed to retrieve DID information for DID %s"
         .formatted(LogUtil.quote(((ProcivisSsiServiceSettings)this.settings.get()).issuerVerifierDid()));
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).listDidsRequestFor(((ProcivisSsiServiceSettings)this.settings.get()).issuerVerifierDid(), false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get())
            .listDidsRequestFor(((ProcivisSsiServiceSettings)this.settings.get()).issuerVerifierDid(), true),
         response -> this.extractDidId(response, errorMessage),
         errorMessage
      );
   }

   private Optional<String> extractDidId(HttpResponse response, Supplier<String> errorMessage) {
      return this.toResponseBodyObject(response.body(), ListDidData.class)
         .map(ListDidData::getValues)
         .filter(values -> values.length == 1)
         .map(values -> (DidItem)values[0])
         .map(DidItem::getId)
         .or(this.loggingEmptySupplier(errorMessage));
   }

   private Optional<String> createProofRequestOrLogError(String proofSchemaId, String proofSchemaName, String didId) {
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get())
            .createProofRequestFor(proofSchemaId, didId, ((ProcivisSsiServiceSettings)this.settings.get()).verificationProtocol(), false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get())
            .createProofRequestFor(proofSchemaId, didId, ((ProcivisSsiServiceSettings)this.settings.get()).verificationProtocol(), true),
         response -> this.toResponseBodyObject(response.body(), CreateData.class).map(CreateData::getId),
         () -> "Failed to create proof request for proof schema %s (ID=%s) and DID %s (ID=%s)"
            .formatted(
               LogUtil.quote(proofSchemaName),
               LogUtil.quote(proofSchemaId),
               LogUtil.quote(((ProcivisSsiServiceSettings)this.settings.get()).issuerVerifierDid()),
               LogUtil.quote(didId)
            )
      );
   }

   private Optional<String> shareProofRequestOrLogError(String proofRequestId, String proofSchemaName) {
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).shareProofRequestFor(proofRequestId, false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get()).shareProofRequestFor(proofRequestId, true),
         response -> this.toResponseBodyObject(response.body(), ShareProofRequestData.class).map(ShareProofRequestData::getUrl),
         () -> "Failed to share proof request with ID %s for proof schema %s and DID %s"
            .formatted(
               LogUtil.quote(proofRequestId),
               LogUtil.quote(proofSchemaName),
               LogUtil.quote(((ProcivisSsiServiceSettings)this.settings.get()).issuerVerifierDid())
            )
      );
   }

   private Result<SsiVerifiedData, SsiPollingStatus> getSsiVerifiedData(ProofRequestParameters parameters, ListProofRequestData proofRequest) {
      List<Entry<SsiAttributeSettings, Optional<String>>> receivedValues = parameters.claims()
         .stream()
         .map(SsiClaimSettings::requestedAttributes)
         .flatMap(Collection::stream)
         .map(attr -> findValueForAttribute(attr, proofRequest))
         .toList();
      List<SsiAttributeSettings> attributesWithoutValues = receivedValues.stream().filter(entry -> entry.getValue().isEmpty()).map(Entry::getKey).toList();
      if (!attributesWithoutValues.isEmpty()) {
         String attributes = attributesWithoutValues.stream().map(SsiAttributeSettings::name).map(LogUtil::quote).collect(Collectors.joining(", "));
         String message = "User did not share the requested attributes or they do not exist in the schema: " + attributes;
         this.logger.info(message);
         this.logIfUserPresent(message);
         return Result.failure(SsiPollingStatus.FAILED);
      }

      Map<String, List<String>> knownAttributeNames = collectKnownAttributeNames(parameters);
      List<SsiClaimSettings> unmatchedClaimSettings = parameters.claims()
         .stream()
         .map(
            claimSetting -> {
               List<ListProofRequestData.ProofInputItem> proofInputsWithMatchingSchemaAndAttributes = this.findMatchingProofInputs(
                  proofRequest, claimSetting, knownAttributeNames.get(claimSetting.schemaName())
               );
               if (proofInputsWithMatchingSchemaAndAttributes.isEmpty()) {
                  this.logger.info("No proof input item matched the claim settings %s".formatted(claimSetting));
                  return Optional.of(claimSetting);
               } else {
                  return this.findSettingWithTrustedIssuerViolation(claimSetting, proofInputsWithMatchingSchemaAndAttributes);
               }
            }
         )
         .flatMap(Optional::stream)
         .toList();
      if (!unmatchedClaimSettings.isEmpty()) {
         return Result.failure(SsiPollingStatus.FAILED);
      }

      Set<SsiAttributeSettings> settings = new HashSet<>();
      receivedValues.stream()
         .map(Entry::getKey)
         .filter(setting -> !settings.add(setting))
         .forEach(
            setting -> this.logger
               .info(
                  "Found duplicate attribute setting (will be ignored): attribute name=%s, provider key=%s"
                     .formatted(LogUtil.quote(setting.name()), LogUtil.quote(setting.providerKey()))
               )
         );
      return Result.result(new SsiVerifiedData(receivedValues.stream().distinct().collect(Collectors.toMap(Entry::getKey, v -> v.getValue().orElseThrow()))));
   }

   private static Map<String, List<String>> collectKnownAttributeNames(ProofRequestParameters parameters) {
      return parameters.claims()
         .stream()
         .map(claimSetting -> Map.entry(claimSetting.schemaName(), claimSetting.requestedAttributes().stream().map(SsiAttributeSettings::name).toList()))
         .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).toList()));
   }

   private List<ListProofRequestData.ProofInputItem> findMatchingProofInputs(
      ListProofRequestData proofRequest, SsiClaimSettings claimSetting, List<String> knownAttributeNames
   ) {
      return Arrays.stream(proofRequest.getProofInputs())
         .filter(item -> claimSetting.schemaName().equals(item.getCredentialSchema().getName()))
         .filter(
            item -> claimSetting.requestedAttributes()
               .stream()
               .map(SsiAttributeSettings::name)
               .allMatch(Arrays.stream(item.getClaims()).map(ListProofRequestData.ProofInputItem.ProofClaimData::getPath).toList()::contains)
         )
         .peek(item -> this.findAndLogUnknownAttributes(item, knownAttributeNames))
         .toList();
   }

   private void findAndLogUnknownAttributes(ListProofRequestData.ProofInputItem proofInputItem, List<String> knownAttributeNames) {
      Arrays.stream(proofInputItem.getClaims())
         .filter(data -> Objects.nonNull(data.getValue()))
         .map(ListProofRequestData.ProofInputItem.ProofClaimData::getPath)
         .filter(attributeName -> knownAttributeNames.stream().noneMatch(attributeName::equals))
         .forEach(attribute -> this.logAttributeNotFound(attribute, proofInputItem.getCredentialSchema().getName()));
   }

   private Optional<SsiClaimSettings> findSettingWithTrustedIssuerViolation(
      SsiClaimSettings claimSetting, List<ListProofRequestData.ProofInputItem> proofInputsWithMatchingSchemaAndAttributes
   ) {
      return proofInputsWithMatchingSchemaAndAttributes.stream()
         .filter(isIssuerTrusted(claimSetting))
         .findAny()
         .map(matchingProofItem -> Optional.empty())
         .orElseGet(
            () -> {
               this.logger
                  .info(
                     "Trusted issuer validation failed for requested attributes %s of schema with name %s. Issuer must be one of %s"
                        .formatted(
                           claimSetting.requestedAttributes().stream().map(SsiAttributeSettings::name).sorted().toList(),
                           LogUtil.logSafeQuote(claimSetting.schemaName()),
                           claimSetting.trustedIssuers().stream().map(IssuerId::id).sorted().toList()
                        )
                  );
               this.logIfUserPresent("Trusted issuer validation failed during verification of proof request.");
               return Optional.of(claimSetting);
            }
         );
   }

   private static Predicate<ListProofRequestData.ProofInputItem> isIssuerTrusted(SsiClaimSettings claimSetting) {
      return item -> claimSetting.trustedIssuers().isEmpty()
         || claimSetting.trustedIssuers().stream().map(IssuerId::id).anyMatch(item.getCredential().getIssuerDid().getDid()::equals);
   }

   private void logAttributeNotFound(String path, String schemaName) {
      this.logger
         .warn(
            LogTypes.CONFIGURATION_ERROR,
            "Procivis returned attribute %s from schema %s, which was not defined in the step configuration."
               .formatted(LogUtil.logSafeQuote(path), LogUtil.logSafeQuote(schemaName))
         );
   }

   private Optional<String> retrieveCredentialSchemaIdOrLogError(String credentialSchemaName) {
      Supplier<String> errorMessage = () -> "Failed to retrieve credential schema for schema name %s".formatted(LogUtil.quote(credentialSchemaName));
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).listCredentialSchemasRequestFor(credentialSchemaName, false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get()).listCredentialSchemasRequestFor(credentialSchemaName, true),
         response -> this.extractCredentialSchemaId(response, errorMessage, credentialSchemaName),
         errorMessage
      );
   }

   private Optional<String> extractCredentialSchemaId(HttpResponse response, Supplier<String> errorMessage, String credentialSchemaName) {
      return this.toResponseBodyObject(response.body(), ListCredentialSchemaData.class)
         .map(ListCredentialSchemaData::getValues)
         .stream()
         .flatMap(Arrays::stream)
         .filter(data -> credentialSchemaName.equals(data.getName()))
         .findFirst()
         .map(ListCredentialSchemaData.CredentialSchemaItem::getId)
         .or(this.loggingEmptySupplier(errorMessage));
   }

   private Optional<CredentialSchemaData> retrieveCredentialSchemaDataOrLogError(String credentialSchemaId, CredentialSchema credentialSchema) {
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).retrieveCredentialSchemaRequestFor(credentialSchemaId, false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get()).retrieveCredentialSchemaRequestFor(credentialSchemaId, true),
         response -> this.toResponseBodyObject(response.body(), CredentialSchemaData.class),
         () -> "Failed to retrieve credential schema data for schema name %s".formatted(LogUtil.quote(credentialSchema.name()))
      );
   }

   private Optional<List<CredentialClaim>> extractCredentialClaims(CredentialSchemaData credentialSchema, Map<String, String> attributes) {
      CredentialSchemaData.CredentialClaimData[] claims = credentialSchema.getClaims();
      int numberOfClaims = claims.length;
      if (attributes.size() != numberOfClaims) {
         this.logger
            .error(
               LogTypes.CONFIGURATION_ERROR,
               "Invalid number of SSI attribute mappings specified for credential schema %s. Defined %d, but schema contains %d."
                  .formatted(LogUtil.quote(credentialSchema.getName()), attributes.size(), numberOfClaims)
            );
         return Optional.empty();
      } else {
         Set<String> attributeKeys = attributes.keySet();
         Set<String> claimKeys = Arrays.stream(claims).map(CredentialSchemaData.CredentialClaimData::getKey).collect(Collectors.toSet());
         if (!attributeKeys.equals(claimKeys)) {
            List<String> unmatchedAttributeKeys = attributeKeys.stream().filter(Predicate.not(claimKeys::contains)).toList();
            this.logger
               .error(
                  LogTypes.CONFIGURATION_ERROR,
                  "Specified SSI attribute mapping names %s do not correspond to the credential schema attribute keys."
                     .formatted(LogUtil.quote(unmatchedAttributeKeys))
               );
            return Optional.empty();
         } else {
            return Optional.of(Arrays.stream(claims).map(claim -> new CredentialClaim(claim.getId(), claim.getKey(), attributes.get(claim.getKey()))).toList());
         }
      }
   }

   private Optional<String> createCredentialOrLogError(String credentialSchemaId, String didId, List<CredentialClaim> claims, CredentialSchema credentialSchema) {
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get())
            .createCredentialRequestFor(
               credentialSchemaId, didId, claims.toArray(CredentialClaim[]::new), ((ProcivisSsiServiceSettings)this.settings.get()).issuanceProtocol(), false
            ),
         () -> ((ProcivisRequestFactory)this.requestFactory.get())
            .createCredentialRequestFor(
               credentialSchemaId, didId, claims.toArray(CredentialClaim[]::new), ((ProcivisSsiServiceSettings)this.settings.get()).issuanceProtocol(), true
            ),
         response -> this.toResponseBodyObject(response.body(), CreateData.class).map(CreateData::getId),
         () -> "Failed to create credential for credential schema %s, check the data types of the attributes in the credential schema vs. the configured attributes"
            .formatted(LogUtil.quote(credentialSchema.name()))
      );
   }

   private Optional<String> issueCredential(String credentialId, String credentialSchemaName) {
      return this.executeRequestAndEvaluateResponse(
         ((ProcivisRequestFactory)this.requestFactory.get()).issueCredentialRequestFor(credentialId, false),
         () -> ((ProcivisRequestFactory)this.requestFactory.get()).issueCredentialRequestFor(credentialId, true),
         response -> this.toResponseBodyObject(response.body(), CredentialIssuanceData.class).map(CredentialIssuanceData::getUrl),
         () -> "Failed to issue credential for credential schema %s".formatted(LogUtil.quote(credentialSchemaName))
      );
   }

   private <T> Optional<T> executeRequestAndEvaluateResponse(
      HttpRequest request, Supplier<HttpRequest> fallbackRequestSupplier, Function<HttpResponse, Optional<T>> evaluateResponse, Supplier<String> errorMessage
   ) {
      this.logger.debug("Sending request to: " + request.uri().getStringValue());
      Result<HttpResponse, Exception> response = this.executeRequest(request);
      if (response.isFailure()) {
         this.logFatalOnExecutionError(errorMessage.get(), request, response.getFailure());
         return Optional.empty();
      }

      if (response.get().status() == 401) {
         this.logger.debug(String.format("Request to %s was not authorized. Retrying with refreshed authentication... ", request.uri().getStringValue()));
         response = this.executeRequest(fallbackRequestSupplier.get());
      }

      if (response.isFailure()) {
         this.logFatalOnExecutionError(errorMessage.get(), request, response.getFailure());
         return Optional.empty();
      } else if (!response.get().hasSuccessStatus()) {
         this.logOnErrorResponse(errorMessage.get(), response.get());
         return Optional.empty();
      } else {
         return evaluateResponse.apply(response.get());
      }
   }

   private Result<HttpResponse, Exception> executeRequest(HttpRequest request) {
      try {
         return Result.result(((HttpClient)this.client.get()).execute(request));
      } catch (Exception e) {
         return Result.failure(e);
      }
   }

   private <T> Optional<T> toResponseBodyObject(String responseBody, Class<T> proofSchemaDataClass) {
      try {
         return Optional.ofNullable((T)this.objectMapper.readValue(responseBody, proofSchemaDataClass));
      } catch (JsonProcessingException e) {
         return Optional.empty();
      }
   }

   private void logFatalOnExecutionError(String message, HttpRequest request, Exception failure) {
      this.logger
         .fatal(LogTypes.WEB_SERVICE, "%s, could not execute request to: %s".formatted(message, LogUtil.quote(request.uri().getStringValue())), failure);
   }

   private void logOnErrorResponse(String message, HttpResponse response) {
      this.logger.debug("Received response with status code %s and body %s".formatted(response.status(), LogUtil.quote(response.body())));
      if (response.status() >= 500) {
         this.logger.fatal(LogTypes.WEB_SERVICE, "%s. Status: %s".formatted(message, response.status()));
      } else {
         this.logger.error(LogTypes.WEB_SERVICE, "%s. Status: %s".formatted(message, response.status()));
      }
   }

   private void logIfUserPresent(String message) {
      ((PrincipalInfo)this.user.get()).getKnownIdentity().map(UserIdentity::id).ifPresent(userId -> this.helpdeskLogger.log(userId, message));
   }

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

   private static class NoResultException extends Exception {
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class ProcivisSsiServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ProcivisSsiServiceSettings> settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<HttpClient> client;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ProcivisRequestFactory> requestFactory;
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<PrincipalInfo> user;
      @SuppressFBWarnings(justification = "generated code")
      private HelpdeskLogger helpDeskLogger;

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

      @SuppressFBWarnings(justification = "generated code")
      public ProcivisSsiService.ProcivisSsiServiceBuilder settings(Provider<ProcivisSsiServiceSettings> settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ProcivisSsiService.ProcivisSsiServiceBuilder client(Provider<HttpClient> client) {
         this.client = client;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ProcivisSsiService.ProcivisSsiServiceBuilder requestFactory(Provider<ProcivisRequestFactory> requestFactory) {
         this.requestFactory = requestFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ProcivisSsiService.ProcivisSsiServiceBuilder logger(ELogger logger) {
         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ProcivisSsiService.ProcivisSsiServiceBuilder user(Provider<PrincipalInfo> user) {
         this.user = user;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ProcivisSsiService.ProcivisSsiServiceBuilder helpDeskLogger(HelpdeskLogger helpDeskLogger) {
         this.helpDeskLogger = helpDeskLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ProcivisSsiService build() {
         return new ProcivisSsiService(this.settings, this.client, this.requestFactory, this.logger, this.user, this.helpDeskLogger);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ProcivisSsiService.ProcivisSsiServiceBuilder(settings="
            + this.settings
            + ", client="
            + this.client
            + ", requestFactory="
            + this.requestFactory
            + ", logger="
            + this.logger
            + ", user="
            + this.user
            + ", helpDeskLogger="
            + this.helpDeskLogger
            + ")";
      }
   }
}
