/*
 * 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.tier.sockets.command;

import static org.apache.geode.internal.cache.execute.ServerFunctionExecutor.DEFAULT_CLIENT_FUNCTION_TIMEOUT;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import org.jetbrains.annotations.NotNull;

import org.apache.geode.annotations.Immutable;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.internal.ExecuteFunctionHelper;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionException;
import org.apache.geode.cache.execute.FunctionInvocationTargetException;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.operations.ExecuteFunctionOperationContext;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.internal.cache.DistributedRegion;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.execute.AbstractExecution;
import org.apache.geode.internal.cache.execute.DistributedRegionFunctionExecutor;
import org.apache.geode.internal.cache.execute.InternalFunctionInvocationTargetException;
import org.apache.geode.internal.cache.execute.MemberMappedArgument;
import org.apache.geode.internal.cache.execute.PartitionedRegionFunctionExecutor;
import org.apache.geode.internal.cache.execute.ServerToClientFunctionResultSender;
import org.apache.geode.internal.cache.execute.ServerToClientFunctionResultSender65;
import org.apache.geode.internal.cache.tier.CachedRegionHelper;
import org.apache.geode.internal.cache.tier.Command;
import org.apache.geode.internal.cache.tier.MessageType;
import org.apache.geode.internal.cache.tier.ServerSideHandshake;
import org.apache.geode.internal.cache.tier.sockets.BaseCommand;
import org.apache.geode.internal.cache.tier.sockets.ChunkedMessage;
import org.apache.geode.internal.cache.tier.sockets.Message;
import org.apache.geode.internal.cache.tier.sockets.Part;
import org.apache.geode.internal.cache.tier.sockets.ServerConnection;
import org.apache.geode.internal.security.AuthorizeRequest;
import org.apache.geode.internal.security.SecurityService;
import org.apache.geode.internal.serialization.KnownVersion;

/**
 * @since GemFire 6.6
 */
public class ExecuteRegionFunction66 extends BaseCommand {

  @Immutable
  private static final ExecuteRegionFunction66 singleton = new ExecuteRegionFunction66();

  public static Command getCommand() {
    return singleton;
  }

  ExecuteRegionFunction66() {}

  @Override
  public void cmdExecute(final @NotNull Message clientMessage,
      final @NotNull ServerConnection serverConnection,
      final @NotNull SecurityService securityService, long start) throws IOException {
    String regionName = null;
    Object function = null;
    Object args = null;
    MemberMappedArgument memberMappedArg = null;
    final boolean isBucketsAsFilter;
    final byte isReExecute;
    Set<Object> filter = null;
    byte hasResult = 0;
    Set<Object> removedNodesSet = null;
    int partNumber = 0;
    byte functionState = 0;
    int functionTimeout = DEFAULT_CLIENT_FUNCTION_TIMEOUT;
    try {
      byte[] bytes = clientMessage.getPart(0).getSerializedForm();
      functionState = bytes[0];
      if (bytes.length >= 5) {
        functionTimeout = Part.decodeInt(bytes, 1);
      }
      if (functionState != 1) {
        hasResult = (byte) ((functionState & 2) - 1);
      } else {
        hasResult = functionState;
      }
      if (hasResult == 1) {
        serverConnection.setAsTrue(REQUIRES_RESPONSE);
        serverConnection.setAsTrue(REQUIRES_CHUNKED_RESPONSE);
      }
      regionName = clientMessage.getPart(1).getCachedString();
      function = clientMessage.getPart(2).getStringOrObject();
      args = clientMessage.getPart(3).getObject();
      Part part = clientMessage.getPart(4);
      memberMappedArg = extractMemberMappedArgument(part);

      byte[] flags = clientMessage.getPart(5).getSerializedForm();
      if (serverConnection.getClientVersion().ordinal() > KnownVersion.GFE_81.ordinal()) {
        isBucketsAsFilter = (flags[0] & ExecuteFunctionHelper.BUCKETS_AS_FILTER_MASK) != 0;
        isReExecute = (flags[0] & ExecuteFunctionHelper.IS_REXECUTE_MASK) != 0 ? (byte) 1 : 0;
      } else {
        isReExecute = flags[0];
        isBucketsAsFilter = false;
      }

      int filterSize = clientMessage.getPart(6).getInt();
      filter = populateFilters(clientMessage, filterSize);
      partNumber = 7 + filterSize;

      int removedNodesSize = clientMessage.getPart(partNumber).getInt();
      removedNodesSet =
          populateRemovedNodes(clientMessage, removedNodesSize, partNumber);

    } catch (ClassNotFoundException exception) {
      logger.warn(String.format("Exception on server while executing function : %s",
          function),
          exception);
      if (hasResult == 1) {
        writeChunkedException(clientMessage, exception, serverConnection);
      } else {
        writeException(clientMessage, exception, false, serverConnection);
      }
      serverConnection.setAsTrue(RESPONDED);
      return;
    }
    if (function == null || regionName == null) {
      String message = generateNullArgumentMessage(regionName, function);
      logger.warn("{}: {}", serverConnection.getName(), message);
      sendError(hasResult, clientMessage, message, serverConnection);
      return;
    }

    CachedRegionHelper crHelper = serverConnection.getCachedRegionHelper();
    Region region = crHelper.getRegion(regionName);
    if (region == null) {
      String message =
          String.format("The region named %s was not found during execute Function request.",
              regionName);
      logger.warn("{}: {}", serverConnection.getName(), message);
      sendError(hasResult, clientMessage, message, serverConnection);
      return;
    }
    ServerSideHandshake handshake = serverConnection.getHandshake();
    int earlierClientReadTimeout = handshake.getClientReadTimeout();
    handshake.setClientReadTimeout(functionTimeout);
    ServerToClientFunctionResultSender resultSender = null;
    Function<?> functionObject = null;
    try {
      if (function instanceof String) {
        functionObject = FunctionService.getFunction((String) function);
        if (!validateFunctionObject(clientMessage, serverConnection, function, hasResult,
            functionState,
            functionObject)) {
          return;
        }
      } else {
        functionObject = (Function) function;
      }

      // check if the caller is authorized to do this operation on server
      functionObject.getRequiredPermissions(regionName, args).forEach(securityService::authorize);
      ExecuteFunctionOperationContext executeContext =
          getAuthorizedExecuteFunctionOperationContext(args, filter,
              functionObject.optimizeForWrite(), serverConnection.getAuthzRequest(),
              functionObject.getId(), region.getFullPath());


      ChunkedMessage m = serverConnection.getFunctionResponseMessage();
      m.setTransactionId(clientMessage.getTransactionId());
      resultSender =
          new ServerToClientFunctionResultSender65(m, MessageType.EXECUTE_REGION_FUNCTION_RESULT,
              serverConnection, functionObject, executeContext);

      AbstractExecution execution =
          createExecution(args, memberMappedArg, isBucketsAsFilter, filter,
              removedNodesSet,
              region, resultSender);
      if (execution instanceof PartitionedRegionFunctionExecutor) {
        if ((hasResult == 1) && filter != null && filter.size() == 1) {
          ServerConnection.executeFunctionOnLocalNodeOnly((byte) 1);
        }
      }
      if (isReExecute == 1) {
        execution = execution.setIsReExecute();
      }
      if (logger.isDebugEnabled()) {
        logger.debug(
            "Executing Function: {} on Server: {} with Execution: {} functionState={} reExecute={} hasResult={}",
            functionObject.getId(), serverConnection, execution, functionState, isReExecute,
            hasResult);
      }
      if (hasResult == 1) {
        executeFunctionWithResult(function, functionState, functionObject, execution);
      } else {
        executeFunctionNoResult(function, functionState, functionObject, execution);
        writeReply(clientMessage, serverConnection);
      }
    } catch (IOException ioe) {
      logger.warn(String.format("Exception on server while executing function : %s",
          function),
          ioe);
      final String message = "Server could not send the reply";
      sendException(hasResult, clientMessage, message, serverConnection, ioe);
    } catch (FunctionException fe) {
      String message = fe.getMessage();
      Object cause = fe.getCause();
      if (cause instanceof FunctionInvocationTargetException
          || cause instanceof QueryInvocationTargetException) {
        logFunctionExceptionCause(function, functionObject, fe, message, cause);
        resultSender.setException(fe);
      } else {
        if (setLastResultReceived(resultSender)) {
          logger.warn(String.format("Exception on server while executing function : %s",
              function),
              fe);
          sendException(hasResult, clientMessage, message, serverConnection, fe);
        }
      }
    } catch (Exception e) {
      if (setLastResultReceived(resultSender)) {
        logger.warn(String.format("Exception on server while executing function : %s",
            function),
            e);
        String message = e.getMessage();
        sendException(hasResult, clientMessage, message, serverConnection, e);
      }
    } finally {
      handshake.setClientReadTimeout(earlierClientReadTimeout);
      ServerConnection.executeFunctionOnLocalNodeOnly((byte) 0);
    }
  }

  void logFunctionExceptionCause(Object function, Function<?> functionObject,
      FunctionException fe, String message, Object cause) {
    if (cause instanceof InternalFunctionInvocationTargetException) {
      // Fix for #44709: User should not be aware of
      // InternalFunctionInvocationTargetException. No instance of
      // InternalFunctionInvocationTargetException is giving useful
      // information to user to take any corrective action hence logging
      // this at fine level logging
      // 1> When bucket is moved
      // 2> Incase of HA FucntionInvocationTargetException thrown. Since
      // it is HA, fucntion will be reexecuted on right node
      // 3> Multiple target nodes found for single hop operation
      // 4> in case of HA member departed
      if (logger.isDebugEnabled()) {
        logger.debug(String.format("Exception on server while executing function: %s",
            function),
            fe);
      }
    } else if (functionObject.isHA()) {
      logger.warn("Exception on server while executing function : {}",
          function + " :" + message);
    } else {
      logger.warn(String.format("Exception on server while executing function : %s",
          function),
          fe);
    }
  }

  AbstractExecution createExecution(Object args, MemberMappedArgument memberMappedArg,
      boolean isBucketsAsFilter, Set<Object> filter,
      Set<Object> removedNodesSet, Region region,
      ServerToClientFunctionResultSender resultSender) {

    AbstractExecution execution = (AbstractExecution) FunctionService.onRegion(region);
    if (execution instanceof PartitionedRegionFunctionExecutor) {
      execution = new PartitionedRegionFunctionExecutor((PartitionedRegion) region, filter, args,
          memberMappedArg, resultSender, removedNodesSet, isBucketsAsFilter);
    } else {
      execution = new DistributedRegionFunctionExecutor((DistributedRegion) region, filter, args,
          memberMappedArg, resultSender);
    }
    return execution;
  }

  boolean validateFunctionObject(Message clientMessage, ServerConnection serverConnection,
      Object function, byte hasResult, byte functionState,
      Function<?> functionObject) throws IOException {
    if (functionObject == null) {
      String message =
          String.format("The function, %s, has not been registered",
              function);
      logger.warn("{}: {}", serverConnection.getName(), message);
      sendError(hasResult, clientMessage, message, serverConnection);
      return false;
    } else {
      byte functionStateOnServerSide = AbstractExecution.getFunctionState(functionObject.isHA(),
          functionObject.hasResult(), functionObject.optimizeForWrite());
      if (logger.isDebugEnabled()) {
        logger.debug("Function State on server side: {} on client: {}",
            functionStateOnServerSide, functionState);
      }
      if (functionStateOnServerSide != functionState) {
        String message =
            String.format("Function attributes at client and server don't match: %s",
                function);
        logger.warn("{}: {}", serverConnection.getName(), message);
        sendError(hasResult, clientMessage, message, serverConnection);
        return false;
      }
    }
    return true;
  }

  String generateNullArgumentMessage(String regionName, Object function) {
    String message = null;
    if (regionName == null) {
      message = "The input region for the execute function request is null";
    } else if (function == null) {
      message = "The input function for the execute function request is null";
    }

    return message;
  }

  MemberMappedArgument extractMemberMappedArgument(Part part)
      throws IOException, ClassNotFoundException {
    MemberMappedArgument memberMappedArg = null;
    if (part != null) {
      Object obj = part.getObject();
      if (obj instanceof MemberMappedArgument) {
        memberMappedArg = (MemberMappedArgument) obj;
      }
    }
    return memberMappedArg;
  }

  Set<Object> populateRemovedNodes(Message clientMessage, int removedNodesSize, int partNumber)
      throws IOException, ClassNotFoundException {
    Set<Object> removedNodesSet = null;
    if (removedNodesSize != 0) {
      removedNodesSet = new HashSet<>();
      partNumber = partNumber + 1;

      for (int i = 0; i < removedNodesSize; i++) {
        removedNodesSet.add(clientMessage.getPart(partNumber + i).getStringOrObject());
      }
    }
    return removedNodesSet;
  }

  Set<Object> populateFilters(Message clientMessage, int filterSize)
      throws IOException, ClassNotFoundException {
    Set<Object> filter = null;
    int partNumber;
    if (filterSize != 0) {
      filter = new HashSet<>();
      partNumber = 7;
      for (int i = 0; i < filterSize; i++) {
        filter.add(clientMessage.getPart(partNumber + i).getStringOrObject());
      }
    }
    return filter;
  }

  ExecuteFunctionOperationContext getAuthorizedExecuteFunctionOperationContext(Object args,
      Set<Object> filter,
      boolean optimizedForWrite,
      AuthorizeRequest authzRequest,
      String functionName,
      String regionPath) {
    ExecuteFunctionOperationContext executeContext = null;
    if (authzRequest != null) {
      executeContext = authzRequest.executeFunctionAuthorize(functionName, regionPath, filter,
          args, optimizedForWrite);
    }
    return executeContext;
  }

  void executeFunctionNoResult(Object function, byte functionState,
      Function<?> functionObject, AbstractExecution execution) {
    if (function instanceof String) {
      switch (functionState) {
        case AbstractExecution.NO_HA_NO_HASRESULT_NO_OPTIMIZEFORWRITE:
          execution.execute((String) function);
          break;
        case AbstractExecution.NO_HA_NO_HASRESULT_OPTIMIZEFORWRITE:
          execution.execute((String) function);
          break;
      }
    } else {
      execution.execute(functionObject);
    }
  }

  void executeFunctionWithResult(Object function, byte functionState,
      Function<?> functionObject, AbstractExecution execution) {
    if (function instanceof String) {
      switch (functionState) {
        case AbstractExecution.NO_HA_HASRESULT_NO_OPTIMIZEFORWRITE:
          execution.execute((String) function).getResult();
          break;
        case AbstractExecution.HA_HASRESULT_NO_OPTIMIZEFORWRITE:
          execution.execute((String) function).getResult();
          break;
        case AbstractExecution.HA_HASRESULT_OPTIMIZEFORWRITE:
          execution.execute((String) function).getResult();
          break;
        case AbstractExecution.NO_HA_HASRESULT_OPTIMIZEFORWRITE:
          execution.execute((String) function).getResult();
          break;
      }
    } else {
      execution.execute(functionObject).getResult();
    }
  }


  private void sendException(byte hasResult, Message msg, String message,
      ServerConnection serverConnection, Throwable e) throws IOException {
    synchronized (msg) {
      if (hasResult == 1) {
        writeFunctionResponseException(msg, MessageType.EXCEPTION, message, serverConnection, e);
      } else {
        writeException(msg, e, false, serverConnection);
      }
      serverConnection.setAsTrue(RESPONDED);
    }
  }

  private void sendError(byte hasResult, Message msg, String message,
      ServerConnection serverConnection) throws IOException {
    synchronized (msg) {
      if (hasResult == 1) {
        writeFunctionResponseError(msg, MessageType.EXECUTE_REGION_FUNCTION_ERROR, message,
            serverConnection);
      } else {
        writeErrorResponse(msg, MessageType.EXECUTE_REGION_FUNCTION_ERROR, message,
            serverConnection);
      }
      serverConnection.setAsTrue(RESPONDED);
    }
  }

  protected static void writeFunctionResponseException(Message origMsg, MessageType messageType,
      String message, ServerConnection serverConnection, Throwable e) throws IOException {
    ChunkedMessage functionResponseMsg = serverConnection.getFunctionResponseMessage();
    ChunkedMessage chunkedResponseMsg = serverConnection.getChunkedResponseMessage();
    int numParts = 0;
    if (functionResponseMsg.headerHasBeenSent()) {
      if (e instanceof FunctionException
          && e.getCause() instanceof InternalFunctionInvocationTargetException) {
        functionResponseMsg.setNumberOfParts(3);
        functionResponseMsg.addObjPart(e);
        functionResponseMsg.addStringPart(BaseCommand.getExceptionTrace(e));
        InternalFunctionInvocationTargetException fe =
            (InternalFunctionInvocationTargetException) e.getCause();
        functionResponseMsg.addObjPart(fe.getFailedNodeSet());
        numParts = 3;
      } else {
        functionResponseMsg.setNumberOfParts(2);
        functionResponseMsg.addObjPart(e);
        functionResponseMsg.addStringPart(BaseCommand.getExceptionTrace(e));
        numParts = 2;
      }
      if (logger.isDebugEnabled()) {
        logger.debug("{}: Sending exception chunk while reply in progress: ",
            serverConnection.getName(), e);
      }
      functionResponseMsg.setServerConnection(serverConnection);
      functionResponseMsg.setLastChunkAndNumParts(true, numParts);
      // functionResponseMsg.setLastChunk(true);
      functionResponseMsg.sendChunk(serverConnection);
    } else {
      chunkedResponseMsg.setMessageType(messageType);
      chunkedResponseMsg.setTransactionId(origMsg.getTransactionId());
      chunkedResponseMsg.sendHeader();
      if (e instanceof FunctionException
          && e.getCause() instanceof InternalFunctionInvocationTargetException) {
        chunkedResponseMsg.setNumberOfParts(3);
        chunkedResponseMsg.addObjPart(e);
        chunkedResponseMsg.addStringPart(BaseCommand.getExceptionTrace(e));
        InternalFunctionInvocationTargetException fe =
            (InternalFunctionInvocationTargetException) e.getCause();
        chunkedResponseMsg.addObjPart(fe.getFailedNodeSet());
        numParts = 3;
      } else {
        chunkedResponseMsg.setNumberOfParts(2);
        chunkedResponseMsg.addObjPart(e);
        chunkedResponseMsg.addStringPart(BaseCommand.getExceptionTrace(e));
        numParts = 2;
      }
      if (logger.isDebugEnabled()) {
        logger.debug("{}: Sending exception chunk: ", serverConnection.getName(), e);
      }
      chunkedResponseMsg.setServerConnection(serverConnection);
      chunkedResponseMsg.setLastChunkAndNumParts(true, numParts);
      chunkedResponseMsg.sendChunk(serverConnection);
    }
  }
}
