/*
 * Copyright (c) 2016, Quancheng-ec.com All right reserved. This software is the confidential and
 * proprietary information of Quancheng-ec.com ("Confidential Information"). You shall not disclose
 * such Confidential Information and shall use it only in accordance with the terms of the license
 * agreement you entered into with Quancheng-ec.com.
 */
package io.github.saluki.grpc.client.internal.unary;

import java.net.SocketAddress;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.util.concurrent.ListenableFuture;
import io.github.saluki.grpc.client.internal.GrpcCallOptions;

import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.Grpc;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.internal.GrpcUtil;

/**
 * @author liushiming 2017年5月2日 下午5:42:42
 * @version FailOverListener.java, v 0.0.1 2017年5月2日 下午5:42:42 liushiming
 */
public class FailOverUnaryFuture<Request, Response> extends ClientCall.Listener<Response>
    implements Runnable {

  private final static Logger logger = LoggerFactory.getLogger(FailOverUnaryFuture.class);

  private final ScheduledExecutorService scheduleRetryService = GrpcUtil.TIMER_SERVICE.create();

  private final AtomicInteger currentRetries = new AtomicInteger(0);

  private final MethodDescriptor<Request, Response> method;

  private CompletionFuture<Response> completionFuture;

  private ClientCall<Request, Response> clientCall;

  private Request request;
  private Response response;
  private Integer maxRetries;
  private boolean enabledRetry;
  private CallOptions callOptions;
  private Channel channel;

  public FailOverUnaryFuture(final MethodDescriptor<Request, Response> method) {
    this.method = method;
  }

  public void setCallOptions(CallOptions callOptions) {
    this.callOptions = callOptions;
  }

  public void setChannel(Channel channel) {
    this.channel = channel;
  }

  public void setMaxRetries(Integer maxRetries) {
    this.maxRetries = maxRetries;
    this.enabledRetry = maxRetries > 0 ? true : false;
  }

  public void setRequest(Request request) {
    this.request = request;
  }

  @Override
  public void onMessage(Response message) {
    if (this.response != null && !enabledRetry) {
      throw Status.INTERNAL.withDescription("More than one value received for unary call")
          .asRuntimeException();
    }
    this.response = message;
  }


  @Override
  public void onClose(Status status, Metadata trailers) {
    try {
      SocketAddress remoteServer = clientCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
      callOptions.getOption(GrpcCallOptions.CALLOPTIONS_CUSTOME_KEY)
          .put(GrpcCallOptions.GRPC_CURRENT_ADDR_KEY, remoteServer);
    } finally {
      if (status.isOk()) {
        statusOk(trailers);
      } else {
        statusError(status, trailers);
      }
    }
  }

  private void statusOk(Metadata trailers) {
    try {
      if (enabledRetry) {
        final NameResolverNotify nameResolverNotify = this.createNameResolverNotify();
        nameResolverNotify.resetChannel();
      }
    } finally {
      if (response == null) {
        completionFuture.setException(Status.INTERNAL
            .withDescription("No value received for unary call").asRuntimeException(trailers));
      }
      completionFuture.set(response);
    }
  }


  private void statusError(Status status, Metadata trailers) {
    if (enabledRetry) {
      final NameResolverNotify nameResolverNotify = this.createNameResolverNotify();
      boolean retryHaveDone = this.retryHaveDone();
      if (retryHaveDone) {
        completionFuture.setException(status.asRuntimeException(trailers));
      } else {
        nameResolverNotify.refreshChannel();
        scheduleRetryService.execute(this);
        SocketAddress remoteAddress =
            (SocketAddress) callOptions.getOption(GrpcCallOptions.CALLOPTIONS_CUSTOME_KEY)
                .get(GrpcCallOptions.GRPC_CURRENT_ADDR_KEY);
        logger.error(String.format("Retrying failed call. Failure #%d,Failure Server: %s",
            currentRetries.get(), String.valueOf(remoteAddress)));
        currentRetries.getAndIncrement();
      }
    } else {
      completionFuture.setException(status.asRuntimeException(trailers));
    }

  }

  private NameResolverNotify createNameResolverNotify() {
    Map<String, Object> affinity = callOptions.getOption(GrpcCallOptions.CALLOPTIONS_CUSTOME_KEY);
    NameResolverNotify nameResolverNotify = NameResolverNotify.newNameResolverNotify();
    nameResolverNotify.refreshAffinity(affinity);
    return nameResolverNotify;
  }

  private boolean retryHaveDone() {
    return currentRetries.get() >= maxRetries;
  }

  @Override
  public void run() {
    this.clientCall = channel.newCall(method, callOptions);
    this.completionFuture = new CompletionFuture<Response>(this.clientCall);
    this.clientCall.start(this, new Metadata());
    this.clientCall.sendMessage(request);
    this.clientCall.halfClose();
    this.clientCall.request(1);
  }


  public ListenableFuture<Response> getFuture() {
    return completionFuture;
  }

  public void cancel() {
    if (clientCall != null) {
      clientCall.cancel("User requested cancelation.", null);
    }
  }

}