/**
 * 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.cxf.clustering;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.cxf.clustering.circuitbreaker.CircuitBreaker;
import org.apache.cxf.clustering.circuitbreaker.ZestCircuitBreaker;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.transport.Conduit;

public class CircuitBreakerTargetSelector extends FailoverTargetSelector {
    public static final int DEFAULT_TIMEOUT = 1000 * 60 /* 1 minute timeout as default */;
    public static final int DEFAULT_THESHOLD = 1;

    private static final String IS_SELECTED = "org.apache.cxf.clustering.CircuitBreakerTargetSelector.IS_SELECTED";
    private static final Logger LOG = LogUtils.getL7dLogger(CircuitBreakerTargetSelector.class);

    /**
     * Static instance of empty (or noop) circuit breaker to handle use cases
     * when alternative addresses or alternative endpoint addresses are nullable
     * (or non-valid).
     */
    private static final  CircuitBreaker NOOP_CIRCUIT_BREAKER = new CircuitBreaker() {
        @Override
        public boolean allowRequest() {
            return true;
        }

        @Override
        public void markFailure(Throwable cause) {
        }

        @Override
        public void markSuccess() {
        }
    };

    private final int threshold;
    private final long timeout;
    private final Map<String, CircuitBreaker> circuits = new LinkedHashMap<>();

    public CircuitBreakerTargetSelector(final int threshold, final long timeout) {
        super();
        this.threshold = threshold;
        this.timeout = timeout;
    }

    public CircuitBreakerTargetSelector(final int threshold, final long timeout,
                                        final String clientBootstrapAddress) {
        super(clientBootstrapAddress);
        this.threshold = threshold;
        this.timeout = timeout;
    }

    public CircuitBreakerTargetSelector() {
        this(DEFAULT_THESHOLD, DEFAULT_TIMEOUT);
    }

    @Override
    public synchronized void setStrategy(FailoverStrategy strategy) {
        super.setStrategy(strategy);

        // Registering the original endpoint in the list of circuit breakers
        if (getEndpoint() != null) {
            final String address = getEndpoint().getEndpointInfo().getAddress();
            if (!StringUtils.isEmpty(address)) {
                circuits.putIfAbsent(address, new ZestCircuitBreaker(threshold, timeout));
            }
        }

        if (strategy != null) {
            final List<String> alternatives = strategy.getAlternateAddresses(null /* no Exchange at this point */);
            if (alternatives != null) {
                for (String alternative: alternatives) {
                    if (!StringUtils.isEmpty(alternative)) {
                        circuits.putIfAbsent(alternative, new ZestCircuitBreaker(threshold, timeout));
                    }
                }
            }
        }
    }
    @Override
    public synchronized Conduit selectConduit(Message message) {
        Conduit c = message.get(Conduit.class);
        if (c != null) {
            return c;
        }
        Exchange exchange = message.getExchange();
        InvocationKey key = new InvocationKey(exchange);
        InvocationContext invocation = inProgress.get(key);
        if (invocation != null && !invocation.getContext().containsKey(IS_SELECTED)) {
            final String address = (String) message.get(Message.ENDPOINT_ADDRESS);

            if (isFailoverRequired(address)) {
                Endpoint target = getFailoverTarget(exchange, invocation);

                if (target == null) {
                    throw new Fault(new FailoverFailedException(
                        "None of alternative addresses are available at the moment"));
                }

                if (isEndpointChanged(address, target)) {
                    setEndpoint(target);
                    message.put(Message.ENDPOINT_ADDRESS, target.getEndpointInfo().getAddress());
                    overrideAddressProperty(invocation.getContext());
                    invocation.getContext().put(IS_SELECTED, null);
                }
            }
        }

        return getSelectedConduit(message);
    }

    @Override
    protected Endpoint getFailoverTarget(final Exchange exchange, final InvocationContext invocation) {
        if (circuits.isEmpty()) {
            LOG.log(Level.SEVERE, "No alternative addresses configured");
            return null;
        }

        final List<String> alternateAddresses = updateContextAlternatives(exchange, invocation);
        if (alternateAddresses != null) {
            final Iterator<String> alternateAddressIterator = alternateAddresses.iterator();

            while (alternateAddressIterator.hasNext()) {
                final String alternateAddress = alternateAddressIterator.next();
                final CircuitBreaker circuitBreaker = getCircuitBreaker(alternateAddress);

                if (!circuitBreaker.allowRequest()) {
                    alternateAddressIterator.remove();
                }
            }
        }

        Endpoint failoverTarget = null;
        if (alternateAddresses != null && !alternateAddresses.isEmpty()) {
            final String alternateAddress = getStrategy().selectAlternateAddress(alternateAddresses);

            // Reuse current endpoint
            if (alternateAddress != null) {
                failoverTarget = getEndpoint();
                failoverTarget.getEndpointInfo().setAddress(alternateAddress);
            }
        } else {
            final List<Endpoint> alternateEndpoints = invocation.getAlternateEndpoints();

            if (alternateEndpoints != null) {
                final Iterator<Endpoint> alternateEndpointIterator = alternateEndpoints.iterator();

                while (alternateEndpointIterator.hasNext()) {
                    final Endpoint endpoint = alternateEndpointIterator.next();
                    final CircuitBreaker circuitBreaker = getCircuitBreaker(endpoint);
                    if (!circuitBreaker.allowRequest()) {
                        alternateEndpointIterator.remove();
                    }
                }
            }

            failoverTarget = getStrategy().selectAlternateEndpoint(alternateEndpoints);
        }

        return failoverTarget;
    }

    @Override
    public void prepare(Message message) {
        super.prepare(message);
    }

    @Override
    protected void onFailure(InvocationContext context, Exception ex) {
        super.onFailure(context, ex);

        final Map<String, Object> requestContext =
            CastUtils.cast((Map<?, ?>)context.getContext().get(Client.REQUEST_CONTEXT));

        if (requestContext != null) {
            final String address = (String)requestContext.get(Message.ENDPOINT_ADDRESS);
            getCircuitBreaker(address).markFailure(ex);
        }
    }

    @Override
    protected void onSuccess(InvocationContext context) {
        super.onSuccess(context);

        final Map<String, Object> requestContext =
            CastUtils.cast((Map<?, ?>)context.getContext().get(Client.REQUEST_CONTEXT));

        if (requestContext != null) {
            final String address = (String)requestContext.get(Message.ENDPOINT_ADDRESS);
            getCircuitBreaker(address).markSuccess();
        }
    }

    private CircuitBreaker getCircuitBreaker(final Endpoint endpoint) {
        return getCircuitBreaker(endpoint.getEndpointInfo().getAddress());
    }

    private synchronized CircuitBreaker getCircuitBreaker(final String alternateAddress) {
        CircuitBreaker circuitBreaker = null;

        if (!StringUtils.isEmpty(alternateAddress)) {
            for (Map.Entry<String, CircuitBreaker> entry: circuits.entrySet()) {
                if (alternateAddress.startsWith(entry.getKey())) {
                    circuitBreaker = entry.getValue();
                    break;
                }
            }

            if (circuitBreaker == null) {
                circuitBreaker = new ZestCircuitBreaker(threshold, timeout);
                circuits.put(alternateAddress, circuitBreaker);
            }
        }

        if (circuitBreaker == null) {
            circuitBreaker = NOOP_CIRCUIT_BREAKER;
        }

        return circuitBreaker;
    }

    private boolean isEndpointChanged(final String address, final Endpoint target) {
        if (!StringUtils.isEmpty(address)) {
            return !address.startsWith(target.getEndpointInfo().getAddress());
        }

        if (getEndpoint().equals(target)) {
            return false;
        }

        return !getEndpoint().getEndpointInfo().getAddress().startsWith(
            target.getEndpointInfo().getAddress());
    }

    private boolean isFailoverRequired(final String address) {
        if (!StringUtils.isEmpty(address)) {
            for (final Map.Entry<String, CircuitBreaker> entry: circuits.entrySet()) {
                if (address.startsWith(entry.getKey())) {
                    return !entry.getValue().allowRequest();
                }
            }
        }

        LOG.log(Level.WARNING, "No circuit breaker present for address: " + address);
        return false;
    }
}
