/**
 * Copyright 2016 Netflix, Inc.
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 com.netflix.dyno.connectionpool.impl.lb;

import com.netflix.dyno.connectionpool.Host;
import com.netflix.dyno.connectionpool.exception.DynoException;
import com.netflix.dyno.connectionpool.exception.TimeoutException;
import com.netflix.dyno.connectionpool.impl.utils.CollectionUtils;
import com.netflix.dyno.connectionpool.impl.utils.CollectionUtils.Predicate;
import com.netflix.dyno.connectionpool.impl.utils.IOUtilities;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.params.HttpConnectionParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;

public class HttpEndpointBasedTokenMapSupplier extends AbstractTokenMapSupplier {


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

    private static final String DefaultServerUrl = "http://{hostname}:{port}/REST/v1/admin/cluster_describe";
    private static final Integer NUM_RETRIES_PER_NODE = 2;
    private static final Integer NUM_RETRIER_ACROSS_NODES = 2;
    private static final Integer defaultPort = 8080;

    private final String serverUrl;

    public HttpEndpointBasedTokenMapSupplier() {
        this(DefaultServerUrl, defaultPort);
    }

    public HttpEndpointBasedTokenMapSupplier(int port) {
        this(DefaultServerUrl, port);
    }

    public HttpEndpointBasedTokenMapSupplier(String url, int port) {
        super(port);

        /**
         * If no port is passed means -1 then we will substitute to defaultPort
         * else the passed one.
         */
        url = url.replace("{port}", (port > -1) ? Integer.toString(port) : Integer.toString(defaultPort));
        serverUrl = url;
    }

    @Override
    public String getTopologyJsonPayload(String hostname) {
        try {
            return getResponseViaHttp(hostname);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Tries to get topology information by randomly trying across nodes.
     */
    @Override
    public String getTopologyJsonPayload(Set<Host> activeHosts) {
        int count = NUM_RETRIER_ACROSS_NODES;
        String response;
        Exception lastEx = null;

        do {
            try {
                response = getTopologyFromRandomNodeWithRetry(activeHosts);
                if (response != null) {
                    return response;
                }
            } catch (Exception e) {
                lastEx = e;
            } finally {
                count--;
            }
        } while ((count > 0));

        if (lastEx != null) {
            if (lastEx instanceof ConnectTimeoutException) {
                throw new TimeoutException("Unable to obtain topology", lastEx);
            }
            throw new DynoException(lastEx);
        } else {
            throw new DynoException("Could not contact dynomite for token map");
        }

    }

    private String getResponseViaHttp(String hostname) throws Exception {

        String url = serverUrl;
        url = url.replace("{hostname}", hostname);

        if (Logger.isDebugEnabled()) {
            Logger.debug("Making http call to url: " + url);
        }

        DefaultHttpClient client = new DefaultHttpClient();
        client.getParams().setParameter(HttpConnectionParams.CONNECTION_TIMEOUT, 2000);
        client.getParams().setParameter(HttpConnectionParams.SO_TIMEOUT, 5000);

        DefaultHttpRequestRetryHandler retryhandler = new DefaultHttpRequestRetryHandler(NUM_RETRIER_ACROSS_NODES,
                true);
        client.setHttpRequestRetryHandler(retryhandler);

        HttpGet get = new HttpGet(url);

        HttpResponse response = client.execute(get);
        int statusCode = response.getStatusLine().getStatusCode();
        if (!(statusCode == 200)) {
            Logger.error("Got non 200 status code from " + url);
            return null;
        }

        InputStream in = null;
        try {
            in = response.getEntity().getContent();
            return IOUtilities.toString(in);
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    /**
     * Finds a random host from the set of active hosts to perform
     * cluster_describe
     *
     * @param activeHosts
     * @return a random host
     */
    public Host getRandomHost(Set<Host> activeHosts) {
        Random random = new Random();

        List<Host> hostsUp = new ArrayList<Host>(CollectionUtils.filter(activeHosts, new Predicate<Host>() {

            @Override
            public boolean apply(Host x) {
                return x.isUp();
            }
        }));

        return hostsUp.get(random.nextInt(hostsUp.size()));
    }

    /**
     * Tries multiple nodes, and it only bubbles up the last node's exception.
     * We want to bubble up the exception in order for the last node to be
     * removed from the connection pool.
     *
     * @param activeHosts
     * @return the topology from cluster_describe
     */
    private String getTopologyFromRandomNodeWithRetry(Set<Host> activeHosts) {
        int count = NUM_RETRIES_PER_NODE;
        String nodeResponse;
        Exception lastEx;
        final Host randomHost = getRandomHost(activeHosts);
        do {
            try {
                lastEx = null;
                nodeResponse = getResponseViaHttp(randomHost.getHostName());
                if (nodeResponse != null) {
                    Logger.info("Received topology from " + randomHost);
                    return nodeResponse;
                }
            } catch (Exception e) {
                Logger.info("cannot get topology from : " + randomHost);
                lastEx = e;
            } finally {
                count--;
            }

        } while ((count > 0));

        if (lastEx != null) {
            if (lastEx instanceof ConnectTimeoutException) {
                throw new TimeoutException("Unable to obtain topology", lastEx).setHost(randomHost);
            }
            throw new DynoException(String.format("Unable to obtain topology from %s", randomHost), lastEx);
        } else {
            throw new DynoException(String.format("Could not contact dynomite manager for token map on %s", randomHost));
        }

    }

}