package com.sap.olingo.jpa.processor.core.query;

import static com.sap.olingo.jpa.processor.core.exception.ODataJPAProcessorException.MessageKeys.ATTRIBUTE_NOT_FOUND;
import static com.sap.olingo.jpa.processor.core.exception.ODataJPAQueryException.MessageKeys.QUERY_PREPARATION_FILTER_ERROR;
import static com.sap.olingo.jpa.processor.core.exception.ODataJPAQueryException.MessageKeys.QUERY_PREPARATION_INVALID_SELECTION_PATH;
import static com.sap.olingo.jpa.processor.core.exception.ODataJPAQueryException.MessageKeys.QUERY_RESULT_ENTITY_TYPE_ERROR;
import static com.sap.olingo.jpa.processor.core.exception.ODataJPAQueryException.MessageKeys.QUERY_RESULT_KEY_PROPERTY_ERROR;
import static org.apache.olingo.commons.api.http.HttpStatusCode.BAD_REQUEST;
import static org.apache.olingo.commons.api.http.HttpStatusCode.INTERNAL_SERVER_ERROR;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.AbstractQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Root;

import org.apache.olingo.commons.api.edm.EdmBindingTarget;
import org.apache.olingo.commons.api.edm.EdmEntitySet;
import org.apache.olingo.commons.api.ex.ODataException;
import org.apache.olingo.commons.api.http.HttpStatusCode;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.uri.UriInfoResource;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceKind;
import org.apache.olingo.server.api.uri.UriResourceProperty;
import org.apache.olingo.server.api.uri.queryoption.SelectItem;
import org.apache.olingo.server.api.uri.queryoption.SelectOption;
import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitException;

import com.sap.olingo.jpa.metadata.core.edm.annotation.EdmQueryExtensionProvider;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAAssociationAttribute;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAAssociationPath;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAAttribute;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPACollectionAttribute;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAElement;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAEntitySet;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAEntityType;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAPath;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAStructuredType;
import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAException;
import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelException;
import com.sap.olingo.jpa.processor.core.api.JPAODataClaimProvider;
import com.sap.olingo.jpa.processor.core.api.JPAODataRequestContextAccess;
import com.sap.olingo.jpa.processor.core.api.JPAServiceDebugger.JPARuntimeMeasurement;
import com.sap.olingo.jpa.processor.core.exception.ODataJPAProcessException;
import com.sap.olingo.jpa.processor.core.exception.ODataJPAProcessorException;
import com.sap.olingo.jpa.processor.core.exception.ODataJPAProcessorException.MessageKeys;
import com.sap.olingo.jpa.processor.core.exception.ODataJPAQueryException;
import com.sap.olingo.jpa.processor.core.filter.JPAFilterComplier;
import com.sap.olingo.jpa.processor.core.filter.JPAFilterCrossComplier;
import com.sap.olingo.jpa.processor.core.filter.JPAFilterRestrictionsWatchDog;
import com.sap.olingo.jpa.processor.core.filter.JPAOperationConverter;
import com.sap.olingo.jpa.processor.core.processor.JPAODataInternalRequestContext;
import com.sap.olingo.jpa.processor.core.properties.JPAProcessorAttribute;

public abstract class JPAAbstractJoinQuery extends JPAAbstractQuery {
  protected static final String ALIAS_SEPARATOR = ".";
  protected final UriInfoResource uriResource;
  protected final CriteriaQuery<Tuple> cq;
  protected final List<JPANavigationPropertyInfo> navigationInfo;
  protected final JPANavigationPropertyInfo lastInfo;
  protected final JPAODataRequestContextAccess requestContext;
  protected Root<?> root; // Start of a navigation
  protected From<?, ?> target; // The entity that shall be returned by the query
  protected Optional<JPAEntitySet> entitySet;

  JPAAbstractJoinQuery(final OData odata, final JPAEntityType jpaEntityType,
      final JPAODataRequestContextAccess requestContext, final List<JPANavigationPropertyInfo> navigationInfo)
      throws ODataException {

    this(odata, jpaEntityType, requestContext.getUriInfo(), requestContext, navigationInfo);
  }

  JPAAbstractJoinQuery(final OData odata, final JPAEntityType jpaEntityType, final UriInfoResource uriInfo,
      final JPAODataRequestContextAccess requestContext, final List<JPANavigationPropertyInfo> navigationInfo)
      throws ODataException {

    super(odata, jpaEntityType, requestContext);
    this.requestContext = requestContext;
    this.locale = requestContext.getLocale();
    this.uriResource = uriInfo;
    this.cq = cb.createTupleQuery();
    this.navigationInfo = navigationInfo;
    this.lastInfo = determineLastInfo(navigationInfo);
    this.entitySet = Optional.empty();
  }

  protected static Optional<JPAEntitySet> determineTargetEntitySet(final JPAODataRequestContextAccess requestContext)
      throws ODataException {

    final EdmBindingTarget bindingTarget = Utility.determineBindingTarget(requestContext.getUriInfo()
        .getUriResourceParts());
    if (bindingTarget instanceof EdmEntitySet)
      return requestContext.getEdmProvider().getServiceDocument().getEntitySet(bindingTarget.getName());
    return Optional.empty();
  }

  @SuppressWarnings("unchecked")
  @Override
  public <T> AbstractQuery<T> getQuery() {
    return (AbstractQuery<T>) cq;
  }

  @SuppressWarnings("unchecked")
  @Override
  public <S, T> From<S, T> getRoot() {
    return (From<S, T>) target;
  }

  /**
   * Applies the $skip and $top options of the OData request to the query. The values are defined as follows:
   * <ul>
   * <li>The $top system query option specifies a non-negative integer n that limits the number of items returned from
   * a collection.
   * <li>The $skip system query option specifies a non-negative integer n that excludes the first n items of the
   * queried collection from the result.
   * </ul>
   * These values can be restricted by a page provided by server driven paging
   * <p>
   * For details see:
   * <a href=
   * "http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part1-protocol/odata-v4.0-errata02-os-part1-protocol-complete.html#_Toc406398306"
   * >OData Version 4.0 Part 1 - 11.2.5.3 System Query Option $top</a> and
   * <a href=
   * "http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part1-protocol/odata-v4.0-errata03-os-part1-protocol-complete.html#_Server-Driven_Paging"
   * >OData Version 4.0 Part 1 - 11.2.5.7 Server-Driven Paging</a>
   *
   * @throws ODataApplicationException
   */
  protected void addTopSkip(final TypedQuery<Tuple> typedQuery) {
    /*
     * Where $top and $skip are used together, $skip MUST be applied before $top, regardless of the order in which they
     * appear in the request.
     * If no unique ordering is imposed through an $orderby query option, the service MUST impose a stable ordering
     * across requests that include $skip.
     *
     * URL example: http://localhost:8080/BuPa/BuPa.svc/Organizations?$count=true&$skip=5
     */

    addTop(typedQuery);
    addSkip(typedQuery);
  }

  protected List<JPAPath> buildEntityPathList(final JPAEntityType jpaEntity) throws ODataApplicationException {

    try {
      return jpaEntity.getPathList();
    } catch (final ODataJPAModelException e) {
      throw new ODataJPAQueryException(e, BAD_REQUEST);
    }
  }

  protected final SelectionPathInfo<JPAPath> buildSelectionAddNavigationAndSelect(final UriInfoResource uriResource,
      final SelectOption select, final SelectionPathInfo<JPAPath> jpaPathList) throws ODataApplicationException,
      ODataJPAModelException {

    final boolean targetIsCollection = determineTargetIsCollection(uriResource);
    final String pathPrefix = Utility.determinePropertyNavigationPrefix(uriResource.getUriResourceParts());

    if (Utility.VALUE_RESOURCE.equals(pathPrefix))
      jpaPathList.getODataSelections().addAll(buildPathValue(jpaEntity));
    else if (select == null || select.getSelectItems().isEmpty() || select.getSelectItems().get(0).isStar()) {
      if (pathPrefix == null || pathPrefix.isEmpty())
        copySelectableProperties(jpaPathList, buildEntityPathList(jpaEntity));
      else {
        expandPath(jpaEntity, jpaPathList, pathPrefix, targetIsCollection);
      }
    } else {
      convertSelectIntoPath(select, jpaPathList, targetIsCollection, pathPrefix);
    }
    return jpaPathList;
  }

  /**
   * Creates the path to all properties that need to be selected from the database. A Property can be included for the
   * following reasons:
   * <ul>
   * <li>It is a key in order to be able to build the links</li>
   * <li>It is part of the $select system query option</li>
   * <li>It is the result of a navigation, which my be restricted by a $select</li>
   * <li>If is required to link $expand with result with the parent result</li>
   * <li>A stream is requested and the property contains the mime type</>
   * </ul>
   * Not included are collection properties.
   *
   * @param uriResource
   * @return
   * @throws ODataApplicationException
   */
  protected SelectionPathInfo<JPAPath> buildSelectionPathList(final UriInfoResource uriResource)
      throws ODataApplicationException {
    // TODO It is also possible to request all actions or functions available for each returned entity:
    // http://host/service/Products?$select=DemoService.*

    try {
      final SelectOption select = uriResource.getSelectOption();
      final SelectionPathInfo<JPAPath> jpaPathList = new SelectionPathInfo<>();
      buildSelectionAddNavigationAndSelect(uriResource, select, jpaPathList);
      buildSelectionAddMimeType(jpaEntity, jpaPathList.getODataSelections());
      buildSelectionAddKeys(jpaEntity, jpaPathList.getODataSelections());
      buildSelectionAddExpandSelection(uriResource, jpaPathList.getODataSelections());
      buildSelectionAddETag(jpaEntity, jpaPathList.getODataSelections());
      return jpaPathList;
    } catch (final ODataJPAModelException e) {
      throw new ODataApplicationException(e.getLocalizedMessage(), INTERNAL_SERVER_ERROR
          .getStatusCode(), ODataJPAException.getLocales().nextElement(), e);
    }
  }

  protected <Y extends Comparable<? super Y>> jakarta.persistence.criteria.Expression<Boolean> createBoundary(
      final List<JPANavigationPropertyInfo> info, final Optional<JPAKeyBoundary> keyBoundary)
      throws ODataJPAQueryException {

    if (keyBoundary.isPresent()) {
      // Given key: Organizations('1')/Roles(...)
      // First is the root
      final JPANavigationPropertyInfo propertyInfo = info.get(keyBoundary.get().getNoHops() - 1);
      try {
        final JPAEntityType et = propertyInfo.getEntityType();
        final From<?, ?> from = propertyInfo.getFromClause();

        if (keyBoundary.get().getKeyBoundary().hasUpperBoundary()) {
          return createBoundaryWithUpper(et, from, keyBoundary.get().getKeyBoundary());
        } else {
          return createBoundaryEquals(et, from, keyBoundary.get().getKeyBoundary());
        }
      } catch (final ODataJPAModelException e) {
        throw new ODataJPAQueryException(e, INTERNAL_SERVER_ERROR);
      }
    }
    return null;
  }

  protected Map<String, From<?, ?>> createFromClause(final List<JPAProcessorAttribute> orderByTarget,
      final Collection<JPAPath> selectionPath, final CriteriaQuery<?> query, final JPANavigationPropertyInfo lastInfo)
      throws ODataApplicationException, JPANoSelectionException {

    final Map<String, From<?, ?>> joinTables = new HashMap<>();
    // 1. Create navigation joins
    createFromClauseRoot(query, joinTables);
    target = root;
    createFromClauseNavigationJoins(joinTables);
    createFromClauseCollectionsJoins(joinTables);
    // 2. OrderBy navigation property
    createFromClauseOrderBy(orderByTarget, joinTables, target);
    // 3. Description Join determine
    createFromClauseDescriptionFields(selectionPath, joinTables, target, navigationInfo);
    // 4. Collection Attribute Joins
    generateCollectionAttributeJoin(joinTables, selectionPath, lastInfo);

    return joinTables;
  }

  protected jakarta.persistence.criteria.Expression<Boolean> createProtectionWhere(
      final Optional<JPAODataClaimProvider> claimsProvider) throws ODataJPAQueryException {

    jakarta.persistence.criteria.Expression<Boolean> restriction = null;
    for (final JPANavigationPropertyInfo navigation : navigationInfo) { // for all participating entity types/tables
      try {
        final JPAEntityType et = navigation.getEntityType();
        final From<?, ?> from = navigation.getFromClause();
        restriction = addWhereClause(restriction, createProtectionWhereForEntityType(claimsProvider, et, from));
      } catch (final ODataJPAModelException e) {
        throw new ODataJPAQueryException(QUERY_RESULT_ENTITY_TYPE_ERROR, INTERNAL_SERVER_ERROR, e);
      }
    }
    return restriction;
  }

  protected jakarta.persistence.criteria.Expression<Boolean> createWhere(final UriInfoResource uriInfo,
      final List<JPANavigationPropertyInfo> navigationInfo) throws ODataApplicationException {

    try (JPARuntimeMeasurement serializerMeasurement = debugger.newMeasurement(this, "createWhere")) {
      jakarta.persistence.criteria.Expression<Boolean> whereCondition = null;
      // Given keys: Organizations('1')/Roles(...)
      whereCondition = createKeyWhere(navigationInfo);
      // http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part1-protocol/odata-v4.0-errata02-os-part1-protocol-complete.html#_Toc406398301
      // http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part2-url-conventions/odata-v4.0-errata02-os-part2-url-conventions-complete.html#_Toc406398094
      // https://tools.oasis-open.org/version-control/browse/wsvn/odata/trunk/spec/ABNF/odata-abnf-construction-rules.txt
      try {
        final JPAFilterComplier compiler = navigationInfo.get(navigationInfo.size() - 1).getFilterCompiler();
        final Expression<Boolean> filter = compiler.compile();
        final Optional<JPAFilterRestrictionsWatchDog> watchDog = compiler.getWatchDog();
        if (watchDog.isPresent()) {
          watchDog.get().watch(filter);
        }
        whereCondition = addWhereClause(whereCondition, filter);

      } catch (final ExpressionVisitException e) {
        throw new ODataJPAQueryException(ODataJPAQueryException.MessageKeys.QUERY_PREPARATION_FILTER_ERROR,
            HttpStatusCode.BAD_REQUEST, e);
      }

      if (uriInfo.getSearchOption() != null && uriInfo.getSearchOption().getSearchExpression() != null)
        whereCondition = addWhereClause(whereCondition,
            requestContext.getDatabaseProcessor().createSearchWhereClause(cb, this.cq, target, jpaEntity, uriInfo
                .getSearchOption()));
      whereCondition = addWhereClause(whereCondition, createWhereEnhancement());

      return whereCondition;
    }
  }

  protected Expression<Boolean> createWhereEnhancement() throws ODataJPAProcessorException {
    return createWhereEnhancement(jpaEntity, target);
  }

  @Override
  protected Expression<Boolean> createWhereEnhancement(final JPAEntityType et, final From<?, ?> from)
      throws ODataJPAProcessorException {
    final Optional<EdmQueryExtensionProvider> queryEnhancement = requestContext.getQueryEnhancement(et);
    if (queryEnhancement.isPresent()) {
      debugger.trace(this, "Query Enhancement found. Add WHERE condition of: %s", queryEnhancement.get().getClass()
          .getName());
      return queryEnhancement.get().getFilterExtension(cb, from);
    }
    return null;
  }

  protected JPANavigationPropertyInfo determineLastInfo(final List<JPANavigationPropertyInfo> navigationInfo) {
    return navigationInfo.isEmpty() ? null : navigationInfo.get(navigationInfo.size() - 1);
  }

  protected final boolean determineTargetIsCollection(final UriInfoResource uriResource) {

    final UriResource last = !uriResource.getUriResourceParts().isEmpty() ? uriResource.getUriResourceParts().get(
        uriResource.getUriResourceParts().size() - 1) : null;
    return (last instanceof final UriResourceProperty property && property.isCollection());
  }

  protected void expandPath(final JPAStructuredType st, final SelectionPathInfo<JPAPath> jpaPathList,
      final String selectItem, final boolean targetIsCollection) throws ODataJPAModelException,
      ODataJPAProcessException {

    final JPAPath selectItemPath = st.getPath(selectItem);
    if (selectItemPath == null)
      throw new ODataJPAQueryException(QUERY_PREPARATION_INVALID_SELECTION_PATH, BAD_REQUEST);
    if (selectItemPath.getLeaf().isComplex()) {
      expandComplexPath(st, jpaPathList, targetIsCollection, selectItemPath);
    } else if (selectItemPath.isTransient()) {
      addTransientAttribute(st, jpaPathList, selectItemPath);
    } else if (!selectItemPath.getLeaf().isCollection()
        || targetIsCollection) {// Primitive Type
      jpaPathList.getODataSelections().add(selectItemPath);
    }
  }

  private void expandComplexPath(final JPAStructuredType st, final SelectionPathInfo<JPAPath> jpaPathList,
      final boolean targetIsCollection, final JPAPath selectItemPath) throws ODataJPAModelException,
      ODataJPAProcessorException {
    final List<JPAPath> child = st.searchChildPath(selectItemPath);
    if (targetIsCollection) {
      for (final JPAPath p : child) {
        if (p.isTransient())
          addTransientAttribute(st, jpaPathList, p);
        else
          jpaPathList.getODataSelections().add(p);
      }
    } else {
      copySelectableProperties(jpaPathList, child);
    }
  }

  private void addTransientAttribute(final JPAStructuredType st, final SelectionPathInfo<JPAPath> jpaPathList,
      final JPAPath path) throws ODataJPAModelException, ODataJPAProcessorException {
    buildRequiredSelections(st, path, jpaPathList.getRequiredSelections());
    jpaPathList.getTransientSelections().add(path);
  }

  /*
   * Create the join condition for a collection property. This attribute can be part of structure type, therefore the
   * path to the collection property needs to be traversed
   */
  protected void generateCollectionAttributeJoin(final Map<String, From<?, ?>> joinTables,
      final Collection<JPAPath> jpaPathList, final JPANavigationPropertyInfoAccess lastInfo)
      throws JPANoSelectionException,
      ODataJPAProcessorException {

    for (final JPAPath path : jpaPathList) {
      // 1. check if path contains collection attribute
      final JPAElement collection = findCollection(lastInfo, path);
      // 2. Check if join exists and create join if not
      addCollection(joinTables, path, collection);
    }
  }

  @Override
  protected Locale getLocale() {
    return locale;
  }

  @Override
  JPAODataRequestContextAccess getContext() {
    return requestContext;
  }

  private void addCollection(final Map<String, From<?, ?>> joinTables, final JPAPath path,
      final JPAElement collection) {

    if (collection != null && !joinTables.containsKey(collection.getExternalName())) {
      From<?, ?> from = target;
      for (final JPAElement element : path.getPath()) {
        from = from.join(element.getInternalName());
        if (element instanceof JPACollectionAttribute) {
          break;
        }
      }
      joinTables.put(collection.getExternalName(), from);
    }
  }

  private void addSkip(final TypedQuery<Tuple> typedQuery) {
    if (lastInfo.getUriInfo().getSkipOption() != null)
      typedQuery.setFirstResult(lastInfo.getUriInfo().getSkipOption().getValue());
  }

  private void addTop(final TypedQuery<Tuple> typedQuery) {
    if (lastInfo.getUriInfo().getTopOption() != null)
      typedQuery.setMaxResults(lastInfo.getUriInfo().getTopOption().getValue());

  }

  // Only for streams e.g. .../OrganizationImages('9')/$value
  // Transient not possible
  private List<JPAPath> buildPathValue(final JPAEntityType jpaEntity)
      throws ODataApplicationException {

    final List<JPAPath> jpaPathList = new ArrayList<>();
    try {
      // Stream value
      jpaPathList.add(jpaEntity.getStreamAttributePath());
      jpaPathList.addAll(jpaEntity.getKeyPath());

    } catch (final ODataJPAModelException e) {
      throw new ODataJPAQueryException(e, HttpStatusCode.BAD_REQUEST);
    }
    return jpaPathList;
  }

  private void buildRequiredSelections(final JPAStructuredType et, final JPAPath transientAttributePath,
      final Set<JPAPath> requitedSelections) throws ODataJPAModelException, ODataJPAProcessorException {

    final StringBuilder pathName = new StringBuilder();
    JPAStructuredType st = et;
    for (int i = 0; i < transientAttributePath.getPath().size() - 1; i++) {
      final JPAElement element = transientAttributePath.getPath().get(i);
      pathName.append(element.getExternalName()).append(JPAPath.PATH_SEPARATOR);
      if (element instanceof final JPAAttribute attribute) {
        st = attribute.getStructuredType();
      }
    }

    for (final String internalName : transientAttributePath.getLeaf().getRequiredProperties()) {
      final String externalName = st.getDeclaredAttribute(internalName)
          .orElseThrow(() -> new ODataJPAProcessorException(ATTRIBUTE_NOT_FOUND,
              HttpStatusCode.INTERNAL_SERVER_ERROR, internalName))
          .getExternalName();
      requitedSelections.add(et.getPath(pathName + externalName, false));
    }
  }

  private void buildSelectionAddETag(final JPAEntityType jpaEntity, final Collection<JPAPath> jpaPathList)
      throws ODataJPAModelException {
    if (jpaEntity.hasEtag())
      jpaPathList.add(jpaEntity.getEtagPath());

  }

  /**
   * In order to be able to link the result of an expand query with the super-ordinate query it is necessary to ensure
   * that the join columns are selected.<br>
   * The same columns are required for the count query, for select as well as order by.
   *
   * @param uriResource
   * @param jpaPathList
   * @throws ODataApplicationException
   * @throws ODataJPAQueryException
   */
  private void buildSelectionAddExpandSelection(final UriInfoResource uriResource,
      final Collection<JPAPath> jpaPathList)
      throws ODataApplicationException {

    final Map<JPAExpandItem, JPAAssociationPath> associationPathList = Utility.determineAssociations(sd, uriResource
        .getUriResourceParts(), uriResource.getExpandOption());
    if (!associationPathList.isEmpty()) {
      final List<JPAPath> tmpPathList = new ArrayList<>(jpaPathList);
      final List<JPAPath> addPathList = new ArrayList<>();

      Collections.sort(tmpPathList);
      for (final Entry<JPAExpandItem, JPAAssociationPath> item : associationPathList.entrySet()) {
        final JPAAssociationPath associationPath = item.getValue();
        try {
          for (final JPAPath joinItem : associationPath.getLeftColumnsList()) {
            final int pathIndex = Collections.binarySearch(tmpPathList, joinItem);
            final int insertIndex = Collections.binarySearch(addPathList, joinItem);
            if (pathIndex < 0 && insertIndex < 0)
              addPathList.add(Math.abs(insertIndex) - 1, joinItem);
          }
        } catch (final ODataJPAModelException e) {
          throw new ODataJPAQueryException(e, HttpStatusCode.BAD_REQUEST);
        }
      }
      jpaPathList.addAll(addPathList);
    }
  }

  private void buildSelectionAddKeys(final JPAEntityType jpaEntity, final Set<JPAPath> jpaPathList)
      throws ODataJPAModelException {

    for (final JPAAttribute key : jpaEntity.getKey()) {
      jpaPathList.add(jpaEntity.getPath(key.getExternalName()));
    }
  }

  private void buildSelectionAddMimeType(final JPAEntityType jpaEntity, final Collection<JPAPath> jpaPathList)
      throws ODataJPAModelException {

    if (jpaEntity.hasStream()) {
      final JPAPath mimeTypeAttribute = jpaEntity.getContentTypeAttributePath();
      if (mimeTypeAttribute != null) {
        jpaPathList.add(mimeTypeAttribute);
      }
    }
  }

  private boolean checkCollectionIsPartOfGroup(final String collectionPath) throws ODataJPAProcessorException {

    try {
      final JPAPath path = jpaEntity.getPath(collectionPath);
      if (path != null)
        return path.isPartOfGroups(groups);
      else
        throw new ODataJPAProcessorException(MessageKeys.QUERY_PREPARATION_ERROR, HttpStatusCode.INTERNAL_SERVER_ERROR);
    } catch (final ODataJPAModelException e) {
      throw new ODataJPAProcessorException(e, HttpStatusCode.INTERNAL_SERVER_ERROR);
    }
  }

  private void convertSelectIntoPath(final SelectOption select, final SelectionPathInfo<JPAPath> jpaPathList,
      final boolean targetIsCollection, final String pathPrefix) throws ODataJPAModelException,
      ODataJPAProcessException {

    for (final SelectItem sItem : select.getSelectItems()) {
      final String pathItem = sItem.getResourcePath().getUriResourceParts()
          .stream()
          .map(path -> (path.getSegmentValue()))
          .collect(Collectors.joining(JPAPath.PATH_SEPARATOR));
      expandPath(jpaEntity, jpaPathList, pathPrefix.isEmpty() ? pathItem : pathPrefix + "/" + pathItem,
          targetIsCollection);
    }
  }

  /**
   * Skips all those properties that are or belong to a collection property or marked as transient. E.g
   * (Organization)Comment or (Person)InhouseAddress/Room
   *
   * @param selectablePathList
   * @param allPathList
   * @throws ODataJPAProcessorException
   * @throws ODataJPAModelException
   */
  private void copySelectableProperties(final SelectionPathInfo<JPAPath> selectablePathList,
      final List<JPAPath> allPathList) throws ODataJPAProcessorException, ODataJPAModelException {
    for (final JPAPath p : allPathList) {
      if (isPropertySelectable(selectablePathList, p))
        selectablePathList.getODataSelections().add(p);
    }
  }

  private boolean isPropertySelectable(final SelectionPathInfo<JPAPath> selectablePathList, final JPAPath p)
      throws ODataJPAModelException, ODataJPAProcessorException {

    boolean selectable = true;
    for (final JPAElement pathElement : p.getPath()) {
      if (pathElement instanceof final JPAAttribute attribute) {
        if (attribute.isTransient()) {
          addTransientAttribute(jpaEntity, selectablePathList, p);
        }
        if (attribute.isCollection() || attribute.isTransient()) {
          selectable = false;
          break;
        }
      }
    }
    return selectable;
  }

  @SuppressWarnings("unchecked")
  private <Y extends Comparable<? super Y>> jakarta.persistence.criteria.Expression<Boolean> createBoundaryEquals(
      final JPAEntityType et, final From<?, ?> from, final JPAKeyPair jpaKeyPair) throws ODataJPAModelException,
      ODataJPAQueryException {

    jakarta.persistence.criteria.Expression<Boolean> whereCondition = null;
    final List<JPAAttribute> keyElements = new ArrayList<>(et.getKey());
    Collections.reverse(keyElements);
    for (final JPAAttribute keyElement : keyElements) {
      final var jpaPath = et.getPath(keyElement.getExternalName());
      if (jpaPath != null) {
        final Path<Y> keyPath = (Path<Y>) ExpressionUtility.<Comparable<?>> convertToCriteriaPath(from, jpaPath
            .getPath());
        final jakarta.persistence.criteria.Expression<Boolean> equalFragment = cb.equal(keyPath, jpaKeyPair.getMin()
            .get(keyElement));
        if (whereCondition == null)
          whereCondition = equalFragment;
        else
          whereCondition = cb.and(whereCondition, equalFragment);
      } else {
        throw new ODataJPAQueryException(QUERY_RESULT_KEY_PROPERTY_ERROR, INTERNAL_SERVER_ERROR, et.getExternalName());
      }
    }
    return whereCondition;
  }

  private <Y extends Comparable<? super Y>> jakarta.persistence.criteria.Expression<Boolean> createBoundaryWithUpper( // NOSONAR
      final JPAEntityType et, final From<?, ?> from, final JPAKeyPair jpaKeyPair) throws ODataJPAModelException,
      ODataJPAQueryException {

    jakarta.persistence.criteria.Expression<Boolean> boundaryExpression = null;
    final List<JPAAttribute> keyElements = new ArrayList<>(et.getKey());
    // For all key attributes expect the last one
    for (int primaryIndex = 0; primaryIndex < keyElements.size() - 1; primaryIndex++) {
      jakarta.persistence.criteria.Expression<Boolean> expression = null;
      for (int secondaryIndex = 0; secondaryIndex < primaryIndex; secondaryIndex++) {
        final BoundaryInfo<Y> info = getBoundaryInfo(et, from, jpaKeyPair, keyElements, secondaryIndex);
        expression = addWhereClause(expression, cb.equal(info.keyPath, info.lowerBoundary));
        expression = addWhereClause(expression, cb.equal(info.keyPath, info.upperBoundary));
      }
      final BoundaryInfo<Y> info = getBoundaryInfo(et, from, jpaKeyPair, keyElements, primaryIndex);
      expression = addWhereClause(expression, cb.greaterThan(info.keyPath, info.lowerBoundary));
      expression = addWhereClause(expression, cb.lessThan(info.keyPath, info.upperBoundary));
      boundaryExpression = orWhereClause(boundaryExpression, expression);
    }
    // Handle last key attribute
    jakarta.persistence.criteria.Expression<Boolean> lowerExpression = null;
    jakarta.persistence.criteria.Expression<Boolean> upperExpression = null;
    for (int secondaryIndex = 0; secondaryIndex < keyElements.size() - 1; secondaryIndex++) {
      final BoundaryInfo<Y> info = getBoundaryInfo(et, from, jpaKeyPair, keyElements, secondaryIndex);
      lowerExpression = addWhereClause(lowerExpression, cb.equal(info.keyPath, info.lowerBoundary));
      upperExpression = addWhereClause(upperExpression, cb.equal(info.keyPath, info.upperBoundary));
    }
    final BoundaryInfo<Y> info = getBoundaryInfo(et, from, jpaKeyPair, keyElements, keyElements.size() - 1);
    lowerExpression = addWhereClause(lowerExpression, cb.greaterThanOrEqualTo(info.keyPath, info.lowerBoundary));
    upperExpression = addWhereClause(upperExpression, cb.lessThanOrEqualTo(info.keyPath, info.upperBoundary));
    if (keyElements.size() == 1) {
      boundaryExpression = addWhereClause(boundaryExpression, lowerExpression);
      boundaryExpression = addWhereClause(boundaryExpression, upperExpression);
    } else {
      boundaryExpression = orWhereClause(boundaryExpression, lowerExpression);
      boundaryExpression = orWhereClause(boundaryExpression, upperExpression);
    }
    return boundaryExpression;
  }

  private <Y extends Comparable<? super Y>> BoundaryInfo<Y> getBoundaryInfo(final JPAEntityType et,
      final From<?, ?> from, final JPAKeyPair jpaKeyPair, final List<JPAAttribute> keyElements,
      final int secondaryIndex)
      throws ODataJPAModelException, ODataJPAQueryException {

    final JPAAttribute keyElement = keyElements.get(secondaryIndex);
    return new BoundaryInfo<Y>(getKeyPath(et, from, keyElement), // NOSONAR
        jpaKeyPair.getMinElement(keyElement),
        jpaKeyPair.getMaxElement(keyElement));
  }

  @SuppressWarnings("unchecked")
  private <Y extends Comparable<? super Y>> Path<Y> getKeyPath(final JPAEntityType et, final From<?, ?> from,
      final JPAAttribute keyElement) throws ODataJPAModelException, ODataJPAQueryException {
    final var jpaPath = et.getPath(keyElement.getExternalName());
    if (jpaPath != null) {
      return (Path<Y>) ExpressionUtility.<Comparable<?>> convertToCriteriaPath(from,
          jpaPath.getPath());
    } else {
      throw new ODataJPAQueryException(QUERY_RESULT_KEY_PROPERTY_ERROR, INTERNAL_SERVER_ERROR, et
          .getExternalName());
    }

  }

  private void createFromClauseCollectionsJoins(final Map<String, From<?, ?>> joinTables)
      throws ODataJPAQueryException, ODataJPAProcessorException {

    try {
      if (lastInfo.getAssociationPath() != null
