/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 *      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.kie.server.client.balancer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;

import org.kie.server.client.balancer.impl.RandomBalancerStrategy;
import org.kie.server.client.balancer.impl.RoundRobinBalancerStrategy;
import org.kie.server.common.rest.KieServerHttpRequest;
import org.kie.server.common.rest.KieServerHttpRequestException;
import org.kie.server.common.rest.NoEndpointFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoadBalancer {

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

    private static final String URL_SEP = "\\|";

    private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    private final BalancerStrategy balancerStrategy;
    private CopyOnWriteArraySet<String> failedEndpoints = new CopyOnWriteArraySet<String>();

    protected LoadBalancer(BalancerStrategy balancerStrategy) {
        this.balancerStrategy = balancerStrategy;
    }

    public String getUrl() throws KieServerHttpRequestException {
        try {
            String selectedUrl = balancerStrategy.next();
            logger.debug("Load balancer {} selected url '{}'", balancerStrategy, selectedUrl);
            return selectedUrl;
        } catch (NoEndpointFoundException e) {
            checkFailedEndpoints();
            throw e;
        }
    }

    public String markAsFailed(String url) {
        
        String baseUrl = balancerStrategy.markAsOffline(url);
        failedEndpoints.add(baseUrl);
        logger.debug("Url '{}' is marked as failed and will be considered offline by {}", url, balancerStrategy);
        
        return baseUrl;
    }

    public void activate(String url) {
        
        String baseUrl = balancerStrategy.markAsOnline(url);
        failedEndpoints.remove(baseUrl);
        logger.debug("Url '{}' is marked as activated and will be considered online by {}", url, balancerStrategy);
    }

    public void close() {
        try {
            executorService.shutdownNow();
        } catch (Exception e) {
            logger.debug("Error when shutting down load balancer executor service");
        }
    }

    public List<String> getAvailableEndpoints() {
        return this.balancerStrategy.getAvailableEndpoints();
    }

    public List<String> getFailedEndpoints() {
        return new ArrayList<String>(failedEndpoints);
    }

    /*
     *  background operations for checking failed endpoints
     */
    public Future<?> checkFailedEndpoints() {
        return executorService.submit(new CheckFailedEndpoints());
    }

    /*
     * factory methods
     */

    public static LoadBalancer getDefault(String urls) {
        String[] endpoints = new String[0];
        if (urls != null) {
            endpoints = urls.split(URL_SEP);
        }
        return getDefault(Arrays.asList(endpoints));
    }

    public static LoadBalancer getDefault(List<String> urls) {
        RoundRobinBalancerStrategy strategy = new RoundRobinBalancerStrategy(urls);
        return new LoadBalancer(strategy);
    }

    public static LoadBalancer forStrategy(String urls, BalancerStrategy.Type type) {
        String[] endpoints = urls.split(URL_SEP);
        return forStrategy(Arrays.asList(endpoints), type);
    }

    public static LoadBalancer forStrategy(List<String> urls, BalancerStrategy.Type type) {
        BalancerStrategy strategy = null;

        switch (type) {
            case RANDOM_STRATEGY:
                strategy = new RandomBalancerStrategy(urls);
                break;
            case ROUND_ROBIN_STRATEGY:
                strategy = new RoundRobinBalancerStrategy(urls);
                break;
        }
        if (strategy == null) {
            throw new IllegalArgumentException("Unknown strategy type " + type);
        }
        return new LoadBalancer(strategy);
    }

    /*
     * Runnable for checks on failed endpoints
     */
    private class CheckFailedEndpoints implements Runnable {

        @Override
        public void run() {
            if (failedEndpoints == null || failedEndpoints.isEmpty()) {
                return;
            }
            logger.debug("Starting to scan if any of the failed endpoints is back online");
            Iterator<String> iterator = failedEndpoints.iterator();

            while(iterator.hasNext()) {
                String failedEndpoint = iterator.next();
                try {
                    KieServerHttpRequest httpRequest =
                            KieServerHttpRequest.newRequest(failedEndpoint).followRedirects(true).timeout(1000);
                    httpRequest.get();

                    logger.debug("Url '{}' is back online, adding it to load balancer", failedEndpoint);
                    // first remove
//                    iterator.remove();
                    // then activate to avoid concurrent modifications on the failedEndpoints
                    activate(failedEndpoint);
                } catch (Exception e) {
                    logger.debug("Url '{}' is still offline due to {}", failedEndpoint, (e.getCause() == null ? e.getMessage() : e.getCause().getMessage()));
                }
            }
        }
    }
}
