/**
 * 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.camel.component.etcd;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;

import mousio.client.promises.ResponsePromise;
import mousio.etcd4j.requests.EtcdKeyGetRequest;
import mousio.etcd4j.responses.EtcdErrorCode;
import mousio.etcd4j.responses.EtcdException;
import mousio.etcd4j.responses.EtcdKeysResponse;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EtcdWatchConsumer extends AbstractEtcdConsumer implements ResponsePromise.IsSimplePromiseResponseHandler<EtcdKeysResponse> {
    private static final Logger LOGGER = LoggerFactory.getLogger(EtcdWatchConsumer.class);
    private static final String OUTDATED_EVENT_MSG = "requested index is outdated and cleared";

    private final EtcdWatchEndpoint endpoint;
    private final EtcdConfiguration configuration;
    private final AtomicLong index;

    public EtcdWatchConsumer(EtcdWatchEndpoint endpoint, Processor processor, EtcdConfiguration configuration, EtcdNamespace namespace, String path) {
        super(endpoint, processor, configuration, namespace, path);

        this.endpoint = endpoint;
        this.configuration = configuration;
        this.index = new AtomicLong(configuration.getFromIndex());
    }

    @Override
    protected void doStart() throws Exception {
        super.doStart();

        watch();
    }

    @Override
    protected void doStop() throws Exception {
        super.doStop();
    }

    @Override
    public void onResponse(ResponsePromise<EtcdKeysResponse> promise) {
        if (!isRunAllowed()) {
            return;
        }

        Exchange exchange = null;
        Throwable throwable = promise.getException();

        if (throwable != null && throwable instanceof EtcdException) {
            EtcdException exception = (EtcdException) throwable;
            // Etcd only keeps the responses of the most recent 1000 events
            // across all etcd keys so if we wait for a cleared index, we
            // get "index is outdated response" like:
            //
            // {
            //     "errorCode" : 401,
            //     "message"   : "The event in requested index is outdated and cleared",
            //     "cause"     : "the requested history has been cleared [1008/8]",
            //     "index"     : 2007
            // }
            //
            // So we set the index to the one returned by the exception + 1
            if (isOutdatedIndexException(exception)) {
                LOGGER.debug("Outdated index, key: {}, cause={}", getPath(), exception.etcdCause);

                // We set the index to the one returned by the exception + 1.
                index.set(exception.index + 1);

                // Clean-up the exception so it is not rethrown/handled
                throwable = null;
            }
        } else {
            try {
                EtcdKeysResponse response = promise.get();

                exchange = endpoint.createExchange();
                exchange.getIn().setHeader(EtcdConstants.ETCD_NAMESPACE, getNamespace());
                exchange.getIn().setHeader(EtcdConstants.ETCD_PATH, response.node.key);
                exchange.getIn().setBody(response);

                // Watch from the modifiedIndex + 1 of the node we got for ensuring
                // no events are missed between watch commands
                index.set(response.node.modifiedIndex + 1);
            } catch (TimeoutException e) {
                LOGGER.debug("Timeout watching for {}", getPath());

                if (configuration.isSendEmptyExchangeOnTimeout()) {
                    exchange = endpoint.createExchange();
                    exchange.getIn().setHeader(EtcdConstants.ETCD_NAMESPACE, getNamespace());
                    exchange.getIn().setHeader(EtcdConstants.ETCD_TIMEOUT, true);
                    exchange.getIn().setHeader(EtcdConstants.ETCD_PATH, getPath());
                    exchange.getIn().setBody(null);
                }

                throwable = null;
            } catch (Exception e1) {
                throwable = e1;
            }

            if (exchange != null) {
                try {
                    throwable = null;
                    getProcessor().process(exchange);
                } catch (Exception e) {
                    getExceptionHandler().handleException("Error processing exchange", exchange, e);
                }
            }
        }

        if (throwable != null) {
            handleException("Error processing etcd response", throwable);
        }

        try {
            watch();
        } catch (Exception e) {
            handleException("Error watching key " + getPath(), e);
        }
    }

    private void watch() throws Exception {
        if (!isRunAllowed()) {
            return;
        }

        EtcdKeyGetRequest request = getClient().get(getPath()).waitForChange(index.get());
        if (configuration.isRecursive()) {
            request.recursive();
        }
        if (configuration.getTimeout() != null) {
            request.timeout(configuration.getTimeout(), TimeUnit.MILLISECONDS);
        }

        request.send().addListener(this);
    }

    private boolean isOutdatedIndexException(EtcdException exception) {
        if (exception.isErrorCode(EtcdErrorCode.EventIndexCleared) && exception.etcdMessage != null) {
            return exception.etcdMessage.toLowerCase().contains(OUTDATED_EVENT_MSG);
        }

        return false;
    }
}
