/**
 * 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.salesforce.internal.streaming;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.camel.CamelException;
import org.apache.camel.component.salesforce.SalesforceEndpointConfig;
import org.apache.camel.component.salesforce.api.SalesforceException;
import org.apache.camel.component.salesforce.api.dto.CreateSObjectResult;
import org.apache.camel.component.salesforce.internal.client.RestClient;
import org.apache.camel.component.salesforce.internal.client.SyncResponseCallback;
import org.apache.camel.component.salesforce.internal.dto.PushTopic;
import org.apache.camel.component.salesforce.internal.dto.QueryRecordsPushTopic;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PushTopicHelper {
    private static final Logger LOG = LoggerFactory.getLogger(PushTopicHelper.class);
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final String PUSH_TOPIC_OBJECT_NAME = "PushTopic";
    private static final long API_TIMEOUT = 60; // Rest API call timeout
    private final SalesforceEndpointConfig config;
    private final String topicName;
    private final RestClient restClient;
    private final boolean preApi29;

    public PushTopicHelper(SalesforceEndpointConfig config, String topicName, RestClient restClient) {
        this.config = config;
        this.topicName = topicName;
        this.restClient = restClient;
        this.preApi29 = Double.valueOf(config.getApiVersion()) < 29.0;

        // validate notify fields right away
        if (preApi29 && (config.getNotifyForOperationCreate() != null
                || config.getNotifyForOperationDelete() != null
                || config.getNotifyForOperationUndelete() != null
                || config.getNotifyForOperationUpdate() != null)) {
            throw new IllegalArgumentException("NotifyForOperationCreate, NotifyForOperationDelete"
                + ", NotifyForOperationUndelete, and NotifyForOperationUpdate"
                + " are only supported since API version 29.0"
                + ", instead use NotifyForOperations");
        } else if (!preApi29 && config.getNotifyForOperations() != null) {
            throw new IllegalArgumentException("NotifyForOperations is readonly since API version 29.0"
                + ", instead use NotifyForOperationCreate, NotifyForOperationDelete"
                + ", NotifyForOperationUndelete, and NotifyForOperationUpdate");
        }
    }

    public void createOrUpdateTopic() throws CamelException {
        final String query = config.getSObjectQuery();

        final SyncResponseCallback callback = new SyncResponseCallback();
        // lookup Topic first
        try {
            // use SOQL to lookup Topic, since Name is not an external ID!!!
            restClient.query("SELECT Id, Name, Query, ApiVersion, IsActive, "
                    + "NotifyForFields, NotifyForOperations, Description "
                    + "FROM PushTopic WHERE Name = '" + topicName + "'",
                    callback);

            if (!callback.await(API_TIMEOUT, TimeUnit.SECONDS)) {
                throw new SalesforceException("API call timeout!", null);
            }
            final SalesforceException callbackException = callback.getException();
            if (callbackException != null) {
                throw callbackException;
            }
            QueryRecordsPushTopic records = OBJECT_MAPPER.readValue(callback.getResponse(),
                    QueryRecordsPushTopic.class);
            if (records.getTotalSize() == 1) {

                PushTopic topic = records.getRecords().get(0);
                LOG.info("Found existing topic {}: {}", topicName, topic);

                // check if we need to update topic
                final boolean notifyOperationsChanged;
                if (preApi29) {
                    notifyOperationsChanged =
                        notEquals(config.getNotifyForOperations(), topic.getNotifyForOperations());
                } else {
                    notifyOperationsChanged =
                        notEquals(config.getNotifyForOperationCreate(), topic.getNotifyForOperationCreate())
                        || notEquals(config.getNotifyForOperationDelete(), topic.getNotifyForOperationDelete())
                        || notEquals(config.getNotifyForOperationUndelete(), topic.getNotifyForOperationUndelete())
                        || notEquals(config.getNotifyForOperationUpdate(), topic.getNotifyForOperationUpdate());
                }
                if (!query.equals(topic.getQuery())
                    || notEquals(config.getNotifyForFields(), topic.getNotifyForFields())
                    || notifyOperationsChanged) {

                    if (!config.isUpdateTopic()) {
                        String msg = "Query doesn't match existing Topic and updateTopic is set to false";
                        throw new CamelException(msg);
                    }

                    // otherwise update the topic
                    updateTopic(topic.getId());
                }

            } else {
                createTopic();
            }

        } catch (SalesforceException e) {
            throw new CamelException(
                    String.format("Error retrieving Topic %s: %s", topicName, e.getMessage()),
                    e);
        } catch (IOException e) {
            throw new CamelException(
                    String.format("Un-marshaling error retrieving Topic %s: %s", topicName, e.getMessage()),
                    e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new CamelException(
                    String.format("Un-marshaling error retrieving Topic %s: %s", topicName, e.getMessage()),
                    e);
        } finally {
            // close stream to close HttpConnection
            if (callback.getResponse() != null) {
                try {
                    callback.getResponse().close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    private void createTopic() throws CamelException {
        final PushTopic topic = new PushTopic();
        topic.setName(topicName);
        topic.setApiVersion(Double.valueOf(config.getApiVersion()));
        topic.setQuery(config.getSObjectQuery());
        topic.setDescription("Topic created by Camel Salesforce component");
        topic.setNotifyForFields(config.getNotifyForFields());
        if (preApi29) {
            topic.setNotifyForOperations(config.getNotifyForOperations());
        } else {
            topic.setNotifyForOperationCreate(config.getNotifyForOperationCreate());
            topic.setNotifyForOperationDelete(config.getNotifyForOperationDelete());
            topic.setNotifyForOperationUndelete(config.getNotifyForOperationUndelete());
            topic.setNotifyForOperationUpdate(config.getNotifyForOperationUpdate());
        }

        LOG.info("Creating Topic {}: {}", topicName, topic);
        final SyncResponseCallback callback = new SyncResponseCallback();
        try {
            restClient.createSObject(PUSH_TOPIC_OBJECT_NAME,
                    new ByteArrayInputStream(OBJECT_MAPPER.writeValueAsBytes(topic)), callback);

            if (!callback.await(API_TIMEOUT, TimeUnit.SECONDS)) {
                throw new SalesforceException("API call timeout!", null);
            }
            final SalesforceException callbackException = callback.getException();
            if (callbackException != null) {
                throw callbackException;
            }

            CreateSObjectResult result = OBJECT_MAPPER.readValue(callback.getResponse(), CreateSObjectResult.class);
            if (!result.getSuccess()) {
                final SalesforceException salesforceException = new SalesforceException(
                        result.getErrors(), HttpStatus.BAD_REQUEST_400);
                throw new CamelException(
                        String.format("Error creating Topic %s: %s", topicName, result.getErrors()),
                        salesforceException);
            }
        } catch (SalesforceException e) {
            throw new CamelException(
                    String.format("Error creating Topic %s: %s", topicName, e.getMessage()),
                    e);
        } catch (IOException e) {
            throw new CamelException(
                    String.format("Un-marshaling error creating Topic %s: %s", topicName, e.getMessage()),
                    e);
        } catch (InterruptedException e) {
            throw new CamelException(
                    String.format("Un-marshaling error creating Topic %s: %s", topicName, e.getMessage()),
                    e);
        } finally {
            if (callback.getResponse() != null) {
                try {
                    callback.getResponse().close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    private void updateTopic(String topicId) throws CamelException {
        final String query = config.getSObjectQuery();
        LOG.info("Updating Topic {} with Query [{}]", topicName, query);

        final SyncResponseCallback callback = new SyncResponseCallback();
        try {
            // update the query, notifyForFields and notifyForOperations fields
            final PushTopic topic = new PushTopic();
            topic.setQuery(query);
            topic.setNotifyForFields(config.getNotifyForFields());
            if (preApi29) {
                topic.setNotifyForOperations(config.getNotifyForOperations());
            } else {
                topic.setNotifyForOperationCreate(config.getNotifyForOperationCreate());
                topic.setNotifyForOperationDelete(config.getNotifyForOperationDelete());
                topic.setNotifyForOperationUndelete(config.getNotifyForOperationUndelete());
                topic.setNotifyForOperationUpdate(config.getNotifyForOperationUpdate());
            }

            restClient.updateSObject("PushTopic", topicId,
                    new ByteArrayInputStream(OBJECT_MAPPER.writeValueAsBytes(topic)),
                    callback);

            if (!callback.await(API_TIMEOUT, TimeUnit.SECONDS)) {
                throw new SalesforceException("API call timeout!", null);
            }
            final SalesforceException callbackException = callback.getException();
            if (callbackException != null) {
                throw callbackException;
            }

        } catch (SalesforceException e) {
            throw new CamelException(
                    String.format("Error updating topic %s with query [%s] : %s", topicName, query, e.getMessage()),
                    e);
        } catch (InterruptedException e) {
            // reset interrupt status
            Thread.currentThread().interrupt();
            throw new CamelException(
                    String.format("Error updating topic %s with query [%s] : %s", topicName, query, e.getMessage()),
                    e);
        } catch (IOException e) {
            throw new CamelException(
                    String.format("Error updating topic %s with query [%s] : %s", topicName, query, e.getMessage()),
                    e);
        } finally {
            if (callback.getResponse() != null) {
                try {
                    callback.getResponse().close();
                } catch (IOException ignore) {
                }
            }
        }
    }

    private static <T> boolean notEquals(T o1, T o2) {
        return o1 != null && !o1.equals(o2);
    }

}