/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.internal.cache;

import static org.apache.geode.internal.cache.LocalRegion.InitializationLevel.ANY_INIT;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.logging.log4j.Logger;

import org.apache.geode.CancelException;
import org.apache.geode.DataSerializer;
import org.apache.geode.SystemFailure;
import org.apache.geode.cache.DynamicRegionFactory;
import org.apache.geode.cache.PartitionAttributes;
import org.apache.geode.cache.RegionDestroyedException;
import org.apache.geode.cache.Scope;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.ClusterDistributionManager;
import org.apache.geode.distributed.internal.DistributionAdvisee;
import org.apache.geode.distributed.internal.DistributionAdvisor;
import org.apache.geode.distributed.internal.DistributionMessage;
import org.apache.geode.distributed.internal.HighPriorityDistributionMessage;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.MessageWithReply;
import org.apache.geode.distributed.internal.ReplyException;
import org.apache.geode.distributed.internal.ReplyMessage;
import org.apache.geode.distributed.internal.ReplyProcessor21;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.cache.CacheDistributionAdvisor.CacheProfile;
import org.apache.geode.internal.cache.CacheDistributionAdvisor.InitialImageAdvice;
import org.apache.geode.internal.cache.LocalRegion.InitializationLevel;
import org.apache.geode.internal.cache.event.EventSequenceNumberHolder;
import org.apache.geode.internal.cache.ha.ThreadIdentifier;
import org.apache.geode.internal.cache.partitioned.Bucket;
import org.apache.geode.internal.cache.partitioned.PRLocallyDestroyedException;
import org.apache.geode.internal.cache.partitioned.RegionAdvisor;
import org.apache.geode.internal.cache.partitioned.RegionAdvisor.PartitionProfile;
import org.apache.geode.internal.cache.persistence.PersistentMemberID;
import org.apache.geode.internal.serialization.DeserializationContext;
import org.apache.geode.internal.serialization.SerializationContext;
import org.apache.geode.logging.internal.log4j.api.LogService;

/**
 * This message processor handles creation and initial exchange of
 * org.apache.geode.internal.cache.CacheDistributionAdvisor.Profiles. It represents creation of a
 * {@link CacheDistributionAdvisee}. Name remains CreateRegion to avoid merge conflicts.
 */
public class CreateRegionProcessor implements ProfileExchangeProcessor {
  private static final Logger logger = LogService.getLogger();

  protected CacheDistributionAdvisee newRegion;

  /** Creates a new instance of CreateRegionProcessor */
  public CreateRegionProcessor(CacheDistributionAdvisee newRegion) {
    this.newRegion = newRegion;
  }

  /** this method tells other members that the region is being created */
  @Override
  public void initializeRegion() {
    InternalDistributedSystem system = newRegion.getSystem();
    // try 5 times, see CreateRegionMessage#skipDuringInitialization
    for (int retry = 0; retry < 5; retry++) {
      Set recps = getRecipients();

      if (logger.isDebugEnabled()) {
        logger.debug("Creating region {}", newRegion);
      }

      if (recps.isEmpty()) {
        if (logger.isDebugEnabled()) {
          logger.debug("CreateRegionProcessor.initializeRegion, no recipients, msg not sent");
        }
        newRegion.getDistributionAdvisor().setInitialized();

        ((LocalRegion) newRegion).getEventTracker().setInitialized();
        return;
      }

      CreateRegionReplyProcessor replyProc = new CreateRegionReplyProcessor(recps);
      newRegion.registerCreateRegionReplyProcessor(replyProc);

      boolean useMcast = false; // multicast is disabled for this message for now
      CreateRegionMessage msg = getCreateRegionMessage(recps, replyProc, useMcast);

      // since PR buckets can be created during cache entry operations, enable
      // severe alert processing if we're creating one of them
      if (((LocalRegion) newRegion).isUsedForPartitionedRegionBucket()) {
        replyProc.enableSevereAlertProcessing();
        msg.severeAlertCompatible = true;
      }

      newRegion.getDistributionManager().putOutgoing(msg);
      // this was in a while() loop, which is incorrect use of a reply processor.
      // Reply procs are deregistered when they return from waitForReplies
      try {
        // Don't allow a region to be created if the distributed system is
        // disconnecting
        newRegion.getCache().getCancelCriterion().checkCancelInProgress(null);

        // This isn't right. We should disable region creation in general, not just
        // the remote case here...
        // // Similarly, don't allow new regions to be created if the cache is closing
        try {
          replyProc.waitForRepliesUninterruptibly();
          if (!replyProc.needRetry()) {
            break;
          }
        } catch (ReplyException e) {
          Throwable t = e.getCause();
          if (t instanceof IllegalStateException) {
            // region is incompatible with region in another cache
            throw (IllegalStateException) t;
          }
          e.handleCause();
          break;
        }
      } finally {
        replyProc.cleanup();
        ((LocalRegion) newRegion).getEventTracker().setInitialized();
        if (((LocalRegion) newRegion).isUsedForPartitionedRegionBucket()) {
          if (logger.isDebugEnabled()) {
            logger.debug("initialized bucket event tracker: {}",
                ((LocalRegion) newRegion).getEventTracker());
          }
        }
      }
    } // while
    // tell advisor that it has been initialized since a profile exchange occurred
    newRegion.getDistributionAdvisor().setInitialized();
  }

  protected Set getRecipients() {
    DistributionAdvisee parent = newRegion.getParentAdvisee();
    Set recps = null;
    if (parent == null) { // root region, all recipients
      InternalDistributedSystem system = newRegion.getSystem();
      recps = system.getDistributionManager().getOtherDistributionManagerIds();
    } else {
      // get recipients that have the parent region defined as distributed.
      recps = getAdvice();
    }
    return recps;
  }

  @Override
  public InitialImageAdvice getInitialImageAdvice(InitialImageAdvice previousAdvice) {
    return newRegion.getCacheDistributionAdvisor().adviseInitialImage(previousAdvice);
  }

  private Set getAdvice() {
    if (newRegion instanceof BucketRegion) {
      return ((Bucket) newRegion).getBucketAdvisor().adviseProfileExchange();
    } else {
      DistributionAdvisee rgn = newRegion.getParentAdvisee();
      DistributionAdvisor advisor = rgn.getDistributionAdvisor();
      return advisor.adviseGeneric();
    }
  }

  protected CreateRegionMessage getCreateRegionMessage(Set recps, ReplyProcessor21 proc,
      boolean useMcast) {
    CreateRegionMessage msg = new CreateRegionMessage();
    msg.regionPath = newRegion.getFullPath();
    msg.profile = (CacheProfile) newRegion.getProfile();
    msg.processorId = proc.getProcessorId();
    msg.concurrencyChecksEnabled = newRegion.getAttributes().getConcurrencyChecksEnabled();
    msg.setMulticast(useMcast);
    msg.setRecipients(recps);
    return msg;
  }

  @Override
  public void setOnline(InternalDistributedMember target) {
    // nothing
  }

  class CreateRegionReplyProcessor extends ReplyProcessor21 {

    CreateRegionReplyProcessor(Set members) {
      super((InternalDistributedSystem) newRegion.getCache()
          .getDistributedSystem(), members);
    }

    private final Map<DistributedMember, Map<ThreadIdentifier, EventSequenceNumberHolder>> remoteEventStates =
        new ConcurrentHashMap<>();

    private boolean allMembersSkippedChecks = true;

    public Map<ThreadIdentifier, EventSequenceNumberHolder> getEventState(
        InternalDistributedMember provider) {
      return remoteEventStates.get(provider);
    }

    /**
     * true if all members skipped CreateRegionMessage#checkCompatibility(), in which case
     * CreateRegionMessage should be retried.
     */
    public boolean needRetry() {
      return allMembersSkippedChecks;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void process(DistributionMessage msg) {
      Assert.assertTrue(msg instanceof CreateRegionReplyMessage,
          "CreateRegionProcessor is unable to process message of type " + msg.getClass());
      CreateRegionReplyMessage reply = (CreateRegionReplyMessage) msg;
      LocalRegion lr = (LocalRegion) newRegion;
      if (logger.isDebugEnabled()) {
        logger.debug("CreateRegionProcessor processing {}", msg);
      }
      try {
        if (reply.profile != null) {
          if (newRegion instanceof DistributedRegion) {
            DistributedRegion dr = (DistributedRegion) newRegion;
            if (!dr.getDataPolicy().withPersistence() && reply.profile.isPersistent) {
              dr.setGeneratedVersionTag(false);
            }
          }
          if (CreateRegionMessage.isLocalAccessor(newRegion) && reply.profile.isPersistent) {
            lr.enableConcurrencyChecks();
          }

          CacheDistributionAdvisor cda =
              newRegion.getCacheDistributionAdvisor();
          cda.putProfile(reply.profile);
          if (reply.bucketProfiles != null) {
            RegionAdvisor ra = (RegionAdvisor) cda;
            ra.putBucketRegionProfiles(reply.bucketProfiles);
          }

          // Save all event states, need to initiate the event tracker from the GII provider
          if (reply.eventState != null) {
            remoteEventStates.put(reply.getSender(),
                (Map<ThreadIdentifier, EventSequenceNumberHolder>) reply.eventState);
          }

          if (lr.isUsedForPartitionedRegionBucket()) {
            ((BucketRegion) lr).updateEventSeqNum(reply.seqKeyForWan);
          }
          // Process any delta filter-profile messages received during profile
          // exchange.
          // The pending messages are queued in the local profile.
          FilterProfile remoteFP = reply.profile.filterProfile;
          if (remoteFP != null) {
            FilterProfile localFP = ((LocalRegion) newRegion).filterProfile;
            // localFP can be null and remoteFP not null when upgrading from 7.0.1.14 to 7.0.1.15
            if (localFP != null) {
              List messages = localFP.getQueuedFilterProfileMsgs(reply.getSender());
              // Thread init level is set since region is used during CQ registration.
              final InitializationLevel oldLevel =
                  LocalRegion.setThreadInitLevelRequirement(ANY_INIT);
              try {
                remoteFP.processQueuedFilterProfileMsgs(messages);
              } finally {
                LocalRegion.setThreadInitLevelRequirement(oldLevel);
                localFP.removeQueuedFilterProfileMsgs(reply.getSender());
              }
            }
          }
        }
        if (reply.destroyedId != null && newRegion instanceof DistributedRegion) {
          DistributedRegion dr = (DistributedRegion) newRegion;
          dr.getPersistenceAdvisor().removeMember(reply.destroyedId);
        }
        if (!reply.skippedCompatibilityChecks) {
          allMembersSkippedChecks = false;
        }
      } finally {
        // invoke super.process() even in case of exceptions (bug #41556)
        if (logger.isDebugEnabled()) {
          logger.debug("CreateRegionProcessor invoking super.process()");
        }
        super.process(msg);
      }
    }

    /**
     * IllegalStateException is an anticipated reply exception. Receiving multiple replies with this
     * exception is normal.
     */
    @Override
    protected boolean logMultipleExceptions() {
      return false;
    }
  }

  public static class CreateRegionMessage extends HighPriorityDistributionMessage
      implements MessageWithReply {

    public boolean concurrencyChecksEnabled;
    protected String regionPath;
    protected CacheProfile profile;
    protected int processorId;

    private transient boolean incompatible = false;
    private transient ReplyException replyException;
    private transient CacheProfile replyProfile;
    private transient ArrayList replyBucketProfiles;
    private transient Object eventState;
    protected transient boolean severeAlertCompatible;
    private transient boolean skippedCompatibilityChecks;

    @Override
    public int getProcessorId() {
      return processorId;
    }

    @Override
    public boolean isSevereAlertCompatible() {
      return severeAlertCompatible;
    }

    @Override
    public boolean sendViaUDP() {
      return true;
    }

    @Override
    protected void process(ClusterDistributionManager dm) {
      // Set thread local flag to allow entrance through initialization Latch
      final InitializationLevel oldLevel = LocalRegion.setThreadInitLevelRequirement(ANY_INIT);
      LocalRegion lclRgn = null;

      PersistentMemberID destroyedId = null;
      try {
        // get the region from the path, but do NOT wait on initialization,
        // otherwise we could have a distributed deadlock

        InternalCache cache = dm.getExistingCache();

        // Fix for bug 42051 - Discover any regions that are in the process
        // of being destroyed
        DistributedRegion destroyingRegion = cache.getRegionInDestroy(regionPath);
        if (destroyingRegion != null) {
          destroyedId = destroyingRegion.getPersistentID();
        }

        lclRgn = (LocalRegion) cache.getRegion(regionPath);

        if (lclRgn instanceof CacheDistributionAdvisee) {
          // bug 37604 - don't return a profile if this is a bucket and the owner
          // has been locally destroyed
          if (lclRgn.isUsedForPartitionedRegionBucket()) {
            if (!((BucketRegion) lclRgn).isPartitionedRegionOpen()) {
              if (logger.isDebugEnabled()) {
                logger.debug("<Partitioned Region Closed or Locally Destroyed> {}", this);
              }
              return;
            }
          }
          handleCacheDistributionAdvisee((CacheDistributionAdvisee) lclRgn, true);
        } else {
          if (lclRgn == null) {
            // check to see if a ProxyBucketRegion (not a true region) exists
            handleCacheDistributionAdvisee(
                PartitionedRegionHelper.getProxyBucketRegion(cache, regionPath), false);
          } else {
            if (logger.isDebugEnabled()) {
              logger.debug("<lclRgn scope is not distributed. Scope={}> {}",
                  lclRgn.getAttributes().getScope(), this);
            }
          }
        }
      } catch (PRLocallyDestroyedException ignore) {
        if (logger.isDebugEnabled()) {
          logger.debug("<Region Locally Destroyed> {}", this);
        }
      } catch (RegionDestroyedException ignore) {
        if (logger.isDebugEnabled()) {
          logger.debug("<RegionDestroyed> {}", this);
        }
      } catch (CancelException ignore) {
        if (logger.isDebugEnabled()) {
          logger.debug("<CancelException> {}", this);
        }
      } catch (VirtualMachineError err) {
        SystemFailure.initiateFailure(err);
        // If this ever returns, rethrow the error. We're poisoned
        // now, so don't let this thread continue.
        throw err;
      } catch (Throwable t) {
        // Whenever you catch Error or Throwable, you must also
        // catch VirtualMachineError (see above). However, there is
        // _still_ a possibility that you are dealing with a cascading
        // error condition, so you also need to check to see if the JVM
        // is still usable:
        SystemFailure.checkFailure();
        if (replyException == null) {
          replyException = new ReplyException(t);
        } else {
          logger.warn(String.format("More than one exception thrown in %s", this),
              t);
        }
      } finally {
        LocalRegion.setThreadInitLevelRequirement(oldLevel);
        CreateRegionReplyMessage replyMsg = new CreateRegionReplyMessage();
        replyMsg.profile = replyProfile;
        replyMsg.bucketProfiles = replyBucketProfiles;
        replyMsg.eventState = eventState;
        replyMsg.destroyedId = destroyedId;
        replyMsg.setProcessorId(processorId);
        replyMsg.setSender(dm.getId()); // for EventStateHelper.dataSerialize
        replyMsg.setRecipient(getSender());
        replyMsg.skippedCompatibilityChecks = skippedCompatibilityChecks;

        if (lclRgn != null && lclRgn.isUsedForPartitionedRegionBucket()) {
          replyMsg.seqKeyForWan = ((BucketRegion) lclRgn).getEventSeqNum().get();
        }
        if (replyException != null && !incompatible) {
          // no need to log the exception if it was caused by compatibility check
          if (logger.isDebugEnabled()) {
            logger.debug("While processing '{}', got exception, returning to sender", this,
                replyException);
          }
        }
        replyMsg.setException(replyException);
        dm.putOutgoing(replyMsg);
        if (lclRgn instanceof PartitionedRegion) {
          ((PartitionedRegion) lclRgn).sendIndexCreationMsg(getSender());
        }
      }
    }

    /**
     * Attempts to process this message with the specified <code>CacheDistributionAdvisee</code>.
     *
     * @param cda the CacheDistributionAdvisee to apply this profile to
     * @param isRealRegion true if CacheDistributionAdvisee is a real region
     */
    private void handleCacheDistributionAdvisee(CacheDistributionAdvisee cda,
        boolean isRealRegion) {
      if (cda == null) {
        // local region or proxy bucket region not found
        if (logger.isDebugEnabled()) {
          logger.debug("<lclRgn is null> {}", this); // matches old logging
        }
        return;
      }
      String errorMsg = null;
      if (isRealRegion) {
        // only check compatibility if this advisee is a real region
        errorMsg = checkCompatibility(cda, profile);
      }
      if (errorMsg != null) {
        incompatible = true;
        if (logger.isDebugEnabled()) {
          logger.debug("{} <replyProfile not set because errorMsg={}", this, errorMsg);
        }
        replyException = new ReplyException(new IllegalStateException(errorMsg));
      } else {
        if (isRealRegion) { // TODO do we need this if clause??
          // if the new member is persistent, turn on concurrency checks
          // fixes bug 45208
          if (isLocalAccessor(cda) && profile.isPersistent) {
            // #45934 need to set the generateVersionTag flag
            if (cda instanceof DistributedRegion) {
              DistributedRegion dr = (DistributedRegion) cda;
              if (!dr.getDataPolicy().withPersistence()) {
                dr.setGeneratedVersionTag(false);
              }
            }

            assert cda instanceof LocalRegion;
            LocalRegion lr = (LocalRegion) cda;
            lr.enableConcurrencyChecks();
          }
        }

        // #45934 don't add profile until the attributes are set correctly,
        // in particular enableConcurrencyChecks and generateVersionTag
        cda.getDistributionAdvisor().putProfile(profile);

        if (isRealRegion) {
          // only exchange profile if this advisee is a real region
          replyProfile = (CacheProfile) cda.getProfile();
          if (cda instanceof PartitionedRegion) {
            // partitioned region needs to also answer back all real bucket profiles
            PartitionedRegion pr = (PartitionedRegion) cda;
            replyBucketProfiles = pr.getRegionAdvisor().getBucketRegionProfiles();
          } else if (((LocalRegion) cda).isUsedForPartitionedRegionBucket()) {
            eventState = ((LocalRegion) cda).getEventState();
          }
        }
      }
    }

    protected String checkCompatibility(CacheDistributionAdvisee rgn, CacheProfile profile) {
      Scope otherScope = rgn.getAttributes().getScope();
      String result = null;

      // Verify both VMs are gateway-enabled or neither are. Note that since
      // this string is sent back to the caller, the 'other' and the 'my'
      // below are from the caller's point of view.
      final DistributedMember myId = rgn.getDistributionManager().getId();
      boolean otherCCEnabled = rgn.getAttributes().getConcurrencyChecksEnabled();
      boolean skipCheckForAccessor = skipCheckForAccessor(rgn, profile);
      boolean skipConcurrencyChecks = skipChecksForInternalRegion(rgn);
      boolean initializing = skipDuringInitialization(rgn);

      if (initializing) {
        skippedCompatibilityChecks = true;
      }

      if (!initializing && !skipCheckForAccessor && (rgn.getAttributes().getDataPolicy()
          .withPersistence() != profile.dataPolicy.withPersistence())) {
        // 45186: Do not allow a persistent replicate to be started if a
        // non-persistent replicate is running
        if (!rgn.getAttributes().getDataPolicy().withPersistence()) {
          result =
              String.format(
                  "Cannot create region %s DataPolicy withPersistence=true because another cache (%s) has the same region DataPolicy withPersistence=false. Persistent members must be started before non-persistent members",
                  regionPath, myId);
          skipConcurrencyChecks = true;
        } else {
          // make the new member turn on concurrency checks
          skipConcurrencyChecks = true;
        }
      }

      if (!initializing && !skipCheckForAccessor && !skipConcurrencyChecks
          && concurrencyChecksEnabled != otherCCEnabled) {
        result =
            String.format(
                "Cannot create region %s concurrency-checks-enabled=%s because another cache (%s) has the same region concurrency-checks-enabled=%s",
                regionPath, concurrencyChecksEnabled, myId, otherCCEnabled);
      }

      Set<String> otherGatewaySenderIds = ((LocalRegion) rgn).getGatewaySenderIds();
      Set<String> myGatewaySenderIds = profile.gatewaySenderIds;
      if (!otherGatewaySenderIds.equals(myGatewaySenderIds)) {
        if (!rgn.getFullPath().contains(DynamicRegionFactory.DYNAMIC_REGION_LIST_NAME)) {
          result =
              String.format(
                  "Cannot create Region %s with %s gateway sender ids because another cache has the same region defined with %s gateway sender ids",
                  regionPath, myGatewaySenderIds, otherGatewaySenderIds);
        }
      }

      Set<String> otherAsynEventQueueIds = ((LocalRegion) rgn).getVisibleAsyncEventQueueIds();
      Set<String> myAsyncEventQueueIds = profile.asyncEventQueueIds;
      if (!isLocalOrRemoteAccessor(rgn, profile)
          && !otherAsynEventQueueIds.equals(myAsyncEventQueueIds)) {
        result =
            String.format(
                "Cannot create Region %s with %s async event ids because another cache has the same region defined with %s async event ids",
                regionPath, myAsyncEventQueueIds, otherAsynEventQueueIds);
      }

      final PartitionAttributes pa = rgn.getAttributes().getPartitionAttributes();
      if (pa == null && profile.isPartitioned) {
        result =
            String.format(
                "Cannot create PartitionedRegion %s because another cache (%s) has the same region defined as a non PartitionedRegion.",
                regionPath, myId);
      } else if (pa != null && !profile.isPartitioned) {
        result =
            String.format(
                "Cannot create the non PartitionedRegion %s because another cache (%s) has a Partitioned Region defined with the same name.",
                regionPath, myId);
      } else if (profile.scope.isDistributed() && otherScope.isDistributed()) {
        // This check is somewhat unnecessary as all Partitioned Regions should have the same scope
        // due to the fact that Partitioned Regions do no support scope.
        if (profile.scope != otherScope) {
          result =
              String.format(
                  "Cannot create region %s with %s scope because another cache (%s) has same region with %s scope.",
                  regionPath, profile.scope, myId, otherScope);
        }
      }

      final boolean otherIsOffHeap = rgn.getAttributes().getOffHeap();

      boolean thisIsRemoteAccessor = !rgn.getAttributes().getDataPolicy().withStorage()
          || (pa != null && pa.getLocalMaxMemory() == 0);

      if (!isRemoteAccessor(profile) && !thisIsRemoteAccessor
          && profile.isOffHeap != otherIsOffHeap) {
        result =
            String.format(
                "Cannot create region %s with off-heap=%s because another cache (%s) has the same region with off-heap=%s.",
                regionPath, profile.isOffHeap, myId, otherIsOffHeap);
      }

      String cspResult = null;
      Map<String, CacheServiceProfile> myProfiles = ((LocalRegion) rgn).getCacheServiceProfiles();
      // Iterate and compare the remote CacheServiceProfiles to the local ones
      for (CacheServiceProfile remoteProfile : profile.cacheServiceProfiles) {
        CacheServiceProfile localProfile = myProfiles.get(remoteProfile.getId());
        if (localProfile == null) {
          cspResult = getMissingProfileMessage(remoteProfile, true);
        } else {
          cspResult = remoteProfile.checkCompatibility(rgn.getFullPath(), localProfile);
        }
        if (cspResult != null) {
          break;
        }
      }

      // If the comparison result is null, compare the local profiles to the remote ones. If there
      // are more local profiles than remote ones (meaning there are ones defined locally that are
      // not defined remotely), then compare those. This should produce an informative error message
      // (as opposed to returning something like 'the profiles don't match').
      if (cspResult == null) {
        if (myProfiles.size() > profile.cacheServiceProfiles.size()) {
          for (CacheServiceProfile localProfile : myProfiles.values()) {
            if (!profile.cacheServiceProfiles.stream()
                .anyMatch(remoteProfile -> remoteProfile.getId().equals(localProfile.getId()))) {
              cspResult = getMissingProfileMessage(localProfile, false);
              break;
            }
          }
        }
      }

      // If the comparison result is not null, set the final result.
      // Note: Be careful not to overwrite the final result with null in case it has already been
      // set in a previous compatibility check.
      if (cspResult != null) {
        result = cspResult;
      }

      if (logger.isDebugEnabled()) {
        logger.debug("CreateRegionProcessor.checkCompatibility: this={}; other={}; result={}", rgn,
            profile, result);
      }

      return result;
    }

    protected String getMissingProfileMessage(CacheServiceProfile profile,
        boolean existsInThisMember) {
      return profile.getMissingProfileMessage(existsInThisMember);
    }

    /**
     * When many members are started concurrently, it is possible that an accessor or non-version
     * generating replicate receives CreateRegionMessage before it is initialized, thus preventing
     * persistent members from starting. We skip compatibilityChecks if the region is not
     * initialized, and let other members check compatibility. If all members skipCompatabilit
     * checks, then the CreateRegionMessage should be retried. fixes #45186
     */
    private boolean skipDuringInitialization(CacheDistributionAdvisee rgn) {
      boolean skip = false;
      if (rgn instanceof LocalRegion) {
        LocalRegion lr = (LocalRegion) rgn;
        if (!lr.isInitialized()) {
          Set recipients = new CreateRegionProcessor(rgn).getRecipients();
          recipients.remove(getSender());
          if (!recipients.isEmpty()) {
            skip = true;
          }
        }
      }
      return skip;
    }

    /**
     * For internal regions skip concurrency-checks-enabled checks, since we will set it to true
     * after profile exchange if required.
     */
    private boolean skipChecksForInternalRegion(CacheDistributionAdvisee rgn) {
      boolean skip = false;
      if (rgn instanceof LocalRegion) {
        LocalRegion lr = (LocalRegion) rgn;
        skip = lr.isInternalRegion();
      }
      return skip;
    }

    /**
     * check for isLocalOrRemoteAccessor(CacheDistributionAdvisee, CacheProfile) and check if
     * DistributedRegion does not generate entry versions.
     */
    private boolean skipCheckForAccessor(CacheDistributionAdvisee rgn, CacheProfile profile) {
      boolean skip = false;
      if (rgn instanceof DistributedRegion) {
        DistributedRegion dr = (DistributedRegion) rgn;
        skip = !dr.getGenerateVersionTag();
      }
      return skip || isLocalOrRemoteAccessor(rgn, profile);
    }

    /**
     * @return true if profile being exchanged or region is an accessor i.e has no storage
     */
    protected static boolean isLocalOrRemoteAccessor(CacheDistributionAdvisee region,
        CacheProfile profile) {
      return isLocalAccessor(region) || isRemoteAccessor(profile);
    }

    protected static boolean isLocalAccessor(CacheDistributionAdvisee region) {
      if (!region.getAttributes().getDataPolicy().withStorage()) {
        return true;
      }
      return region.getAttributes().getPartitionAttributes() != null
          && region.getAttributes().getPartitionAttributes().getLocalMaxMemory() == 0;
    }

    protected static boolean isRemoteAccessor(CacheProfile profile) {
      if (!profile.dataPolicy.withStorage()) {
        return true;
      }
      if (profile.isPartitioned) {
        PartitionProfile prProfile = (PartitionProfile) profile;
        return prProfile.localMaxMemory == 0;
      }
      return false;
    }

    @Override
    public void reset() {
      super.reset();
      regionPath = null;
      profile = null;
      processorId = -1;
    }

    @Override
    public void fromData(DataInput in,
        DeserializationContext context) throws IOException, ClassNotFoundException {
      super.fromData(in, context);
      regionPath = DataSerializer.readString(in);
      profile = DataSerializer.readObject(in);
      processorId = in.readInt();
      concurrencyChecksEnabled = in.readBoolean();
    }

    @Override
    public int getDSFID() {
      return CREATE_REGION_MESSAGE;
    }

    @Override
    public void toData(DataOutput out,
        SerializationContext context) throws IOException {
      super.toData(out, context);
      DataSerializer.writeString(regionPath, out);
      DataSerializer.writeObject(profile, out);
      out.writeInt(processorId);
      out.writeBoolean(concurrencyChecksEnabled);
    }

    @Override
    public String toString() {
      return "CreateRegionMessage (region='"
          + regionPath
          + "'; processorId="
          + processorId
          + "; concurrencyChecksEnabled=" + concurrencyChecksEnabled
          + "; profile="
          + profile
          + ")";
    }
  }

  public static class CreateRegionReplyMessage extends ReplyMessage {
    protected CacheProfile profile;
    protected ArrayList bucketProfiles;
    protected Object eventState;
    /**
     * Added to fix 42051. If the region is in the middle of being destroyed, return the destroyed
     * profile
     */
    protected PersistentMemberID destroyedId;
    protected boolean skippedCompatibilityChecks;

    long seqKeyForWan = -1;

    @Override
    public int getDSFID() {
      return CREATE_REGION_REPLY_MESSAGE;
    }

    @Override
