/*
 * 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.zeppelin.notebook.repo.zeppelinhub.rest;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.net.ssl.SSLContext;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.nio.conn.NoopIOSessionStrategy;
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOReactorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is http client class for the case of proxy usage
 * jetty-client has issue with https over proxy for 9.2.x
 *   https://github.com/eclipse/jetty.project/issues/408
 *   https://github.com/eclipse/jetty.project/issues/827
 *    
 */

public class HttpProxyClient {
  private static final Logger LOG = LoggerFactory.getLogger(HttpProxyClient.class);
  public static final String ZEPPELIN_TOKEN_HEADER = "X-Zeppelin-Token";
  
  private CloseableHttpAsyncClient client;
  private URI proxyUri;
  
  public static HttpProxyClient newInstance(URI proxyUri) {
    return new HttpProxyClient(proxyUri);
  }
  
  private HttpProxyClient(URI uri) {
    this.proxyUri = uri;
    
    client = getAsyncProxyHttpClient(proxyUri);
    client.start();
  }
  
  public URI getProxyUri() {
    return proxyUri;
  }
  
  private CloseableHttpAsyncClient getAsyncProxyHttpClient(URI proxyUri) {
    LOG.info("Creating async proxy http client");
    PoolingNHttpClientConnectionManager cm = getAsyncConnectionManager();
    HttpHost proxy = new HttpHost(proxyUri.getHost(), proxyUri.getPort());
    
    HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
    if (cm != null) {
      clientBuilder = clientBuilder.setConnectionManager(cm);
    }

    if (proxy != null) {
      clientBuilder = clientBuilder.setProxy(proxy);
    }
    clientBuilder = setRedirects(clientBuilder);
    return clientBuilder.build();
  }
  
  private PoolingNHttpClientConnectionManager getAsyncConnectionManager() {
    ConnectingIOReactor ioReactor = null;
    PoolingNHttpClientConnectionManager cm = null;
    try {
      ioReactor = new DefaultConnectingIOReactor();
      // ssl setup
      SSLContext sslcontext = SSLContexts.createSystemDefault();
      X509HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifier();
      @SuppressWarnings("deprecation")
      Registry<SchemeIOSessionStrategy> sessionStrategyRegistry = RegistryBuilder
          .<SchemeIOSessionStrategy>create()
          .register("http", NoopIOSessionStrategy.INSTANCE)
          .register("https", new SSLIOSessionStrategy(sslcontext, hostnameVerifier))
          .build();

      cm = new PoolingNHttpClientConnectionManager(ioReactor, sessionStrategyRegistry);
    } catch (IOReactorException e) {
      LOG.error("Couldn't initialize multi-threaded async client ", e);
      return null;
    }
    return cm;
  }
  
  private HttpAsyncClientBuilder setRedirects(HttpAsyncClientBuilder clientBuilder) {
    clientBuilder.setRedirectStrategy(new DefaultRedirectStrategy() {
      /** Redirectable methods. */
      private String[] REDIRECT_METHODS = new String[] { 
        HttpGet.METHOD_NAME, HttpPost.METHOD_NAME, 
        HttpPut.METHOD_NAME, HttpDelete.METHOD_NAME, HttpHead.METHOD_NAME 
      };

      @Override
      protected boolean isRedirectable(String method) {
        for (String m : REDIRECT_METHODS) {
          if (m.equalsIgnoreCase(method)) {
            return true;
          }
        }
        return false;
      }
    });
    return clientBuilder;
  }
  
  public String sendToZeppelinHub(HttpRequestBase request,
      boolean withResponse) throws IOException {
    return withResponse ?
        sendAndGetResponse(request) : sendWithoutResponseBody(request);
  }
  

  private String sendWithoutResponseBody(HttpRequestBase request) throws IOException {
    FutureCallback<HttpResponse> callback = getCallback(request);
    client.execute(request, callback);
    return StringUtils.EMPTY;
  }
  
  private String sendAndGetResponse(HttpRequestBase request) throws IOException {
    String data = StringUtils.EMPTY;
    try {
      HttpResponse response = client.execute(request, null).get(30, TimeUnit.SECONDS);
      int code = response.getStatusLine().getStatusCode();
      if (code == 200) {
        try (InputStream responseContent = response.getEntity().getContent()) {
          data = IOUtils.toString(responseContent, "UTF-8");
        }
      } else {
        LOG.error("ZeppelinHub {} {} returned with status {} ", request.getMethod(),
            request.getURI(), code);
        throw new IOException("Cannot perform " + request.getMethod() + " request to ZeppelinHub");
      }
    } catch (InterruptedException | ExecutionException | TimeoutException
        | NullPointerException e) {
      throw new IOException(e);
    }
    return data;
  }
  
  private FutureCallback<HttpResponse> getCallback(final HttpRequestBase request) {
    return new FutureCallback<HttpResponse>() {

      public void completed(final HttpResponse response) {
        request.releaseConnection();
        LOG.info("Note {} completed with {} status", request.getMethod(),
            response.getStatusLine());
      }

      public void failed(final Exception ex) {
        request.releaseConnection();
        LOG.error("Note {} failed with {} message", request.getMethod(),
            ex.getMessage());
      }

      public void cancelled() {
        request.releaseConnection();
        LOG.info("Note {} was canceled", request.getMethod());
      }
    };
  }
  
  public void stop() {
    try {
      client.close();
    } catch (Exception e) {
      LOG.error("Failed to close proxy client ", e);
    }
  }
}