/*
 * Copyright (c) 2015, Jurriaan Mous and contributors as indicated by the @author tags.
 *
 * Licensed 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 mousio.etcd4j.transport;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.concurrent.Promise;
import mousio.client.exceptions.PrematureDisconnectException;
import mousio.etcd4j.requests.EtcdRequest;
import mousio.etcd4j.responses.EtcdAuthenticationException;
import mousio.etcd4j.responses.EtcdException;
import mousio.etcd4j.responses.EtcdResponseDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Jurriaan Mous
 * @author Luca Burgazzoli
 *
 * Handles etcd responses
 *
 * @param <R> Response type
 */
class EtcdResponseHandler<R> extends SimpleChannelInboundHandler<FullHttpResponse> {
  private static final Logger logger = LoggerFactory.getLogger(EtcdResponseHandler.class);
  private static final Map<HttpResponseStatus, EtcdResponseDecoder<? extends Throwable>> failureDecoders;

  static {
    failureDecoders = new HashMap<>();
    failureDecoders.put(HttpResponseStatus.UNAUTHORIZED, EtcdAuthenticationException.DECODER);
    failureDecoders.put(HttpResponseStatus.NOT_FOUND, EtcdException.DECODER);
    failureDecoders.put(HttpResponseStatus.FORBIDDEN, EtcdException.DECODER);
    failureDecoders.put(HttpResponseStatus.PRECONDITION_FAILED, EtcdException.DECODER);
    failureDecoders.put(HttpResponseStatus.INTERNAL_SERVER_ERROR, EtcdException.DECODER);
    failureDecoders.put(HttpResponseStatus.BAD_REQUEST, EtcdException.DECODER);
  }

  protected final Promise<R> promise;
  protected final EtcdNettyClient client;
  protected final EtcdRequest<R> request;

  private boolean isRetried;

  /**
   * Constructor
   *
   * @param etcdNettyClient the client handling connections
   * @param etcdRequest     request
   */
  @SuppressWarnings("unchecked")
  public EtcdResponseHandler(EtcdNettyClient etcdNettyClient, EtcdRequest<R> etcdRequest) {
    this.client = etcdNettyClient;
    this.request = etcdRequest;
    this.promise = etcdRequest.getPromise().getNettyPromise();
    this.isRetried = false;
  }

  /**
   * Set if the connection is retried.
   * If true the promise will not fail on un-registering this handler.
   *
   * @param retried true if request is being retried.
   */
  public void retried(boolean retried) {
    this.isRetried = retried;
  }

  @Override
  public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
    if (!isRetried && !promise.isDone()) {
      this.request.getPromise().handleRetry(new PrematureDisconnectException());
    }
    super.channelUnregistered(ctx);
  }

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) throws Exception {
    final HttpResponseStatus status =response.status();
    final HttpHeaders headers = response.headers();
    final ByteBuf content = response.content();

    if (logger.isDebugEnabled()) {
      logger.debug("Received {} for {} {}",
        status.code(), this.request.getMethod().name(), this.request.getUri());
    }

    if (status.equals(HttpResponseStatus.MOVED_PERMANENTLY)
      || status.equals(HttpResponseStatus.TEMPORARY_REDIRECT)) {
      if (headers.contains(HttpHeaderNames.LOCATION)) {
        this.request.setUrl(headers.get(HttpHeaderNames.LOCATION));
        this.client.connect(this.request);
        // Closing the connection which handled the previous request.
        ctx.close();
        if (logger.isDebugEnabled()) {
          logger.debug("redirect for {} to {}",
            this.request.getHttpRequest().uri() ,
            headers.get(HttpHeaderNames.LOCATION)
          );
        }
      } else {
        this.promise.setFailure(new Exception("Missing Location header on redirect"));
      }
    } else {
      EtcdResponseDecoder<? extends Throwable> failureDecoder = failureDecoders.get(status);
      if(failureDecoder != null) {
        this.promise.setFailure(failureDecoder.decode(headers, content));
      } else if (!content.isReadable()) {
        // If connection was accepted maybe response has to be waited for
        if (!status.equals(HttpResponseStatus.OK)
          && !status.equals(HttpResponseStatus.ACCEPTED)
          && !status.equals(HttpResponseStatus.CREATED)) {
          this.promise.setFailure(new IOException(
            "Content was not readable. HTTP Status: " + status));
        }
      } else {
        try {
          this.promise.setSuccess(
            request.getResponseDecoder().decode(headers, content));
        } catch (Exception e) {
          if (e instanceof EtcdException) {
            this.promise.setFailure(e);
          } else {
            try {
              // Try to be smart, if an exception is thrown, first try to decode
              // the content and see if it is an EtcdException, i.e. an error code
              // not included in failureDecoders
              this.promise.setFailure(EtcdException.DECODER.decode(headers, content));
            } catch (Exception e1) {
              // if it fails again, set the original exception as failure
              this.promise.setFailure(e);
            }
          }
        }
      }
    }
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception  {
    this.promise.setFailure(cause);
  }
}