/*
 * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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.wso2.carbon.event.publisher.core;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.deployment.AbstractDeployer;
import org.apache.axis2.deployment.DeploymentException;
import org.apache.axis2.deployment.repository.util.DeploymentFileData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.event.application.deployer.EventProcessingDeployer;
import org.wso2.carbon.event.output.adapter.core.TenantConfigHolder;
import org.wso2.carbon.event.publisher.core.config.EventPublisherConfiguration;
import org.wso2.carbon.event.publisher.core.config.EventPublisherConfigurationFile;
import org.wso2.carbon.event.publisher.core.config.EventPublisherConstants;
import org.wso2.carbon.event.publisher.core.exception.EventPublisherConfigurationException;
import org.wso2.carbon.event.publisher.core.exception.EventPublisherStreamValidationException;
import org.wso2.carbon.event.publisher.core.exception.EventPublisherValidationException;
import org.wso2.carbon.event.publisher.core.internal.CarbonEventPublisherService;
import org.wso2.carbon.event.publisher.core.internal.ds.EventPublisherServiceValueHolder;
import org.wso2.carbon.event.publisher.core.internal.util.EventPublisherConfigurationBuilder;
import org.wso2.carbon.event.publisher.core.internal.util.helper.EventPublisherConfigurationFilesystemInvoker;
import org.wso2.carbon.event.publisher.core.internal.util.helper.EventPublisherConfigurationHelper;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Deploy event publisher as axis2 service
 */
public class EventPublisherDeployer extends AbstractDeployer implements EventProcessingDeployer {

    private static Log log = LogFactory.getLog(EventPublisherDeployer.class);
    private ConfigurationContext configurationContext;
    private Set<String> deployedEventPublisherFilePaths = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
    private Set<String> undeployedEventPublisherFilePaths = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

    public void init(ConfigurationContext configurationContext) {
        this.configurationContext = configurationContext;
        TenantConfigHolder.addTenantConfig(PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(), configurationContext);
    }

    /**
     * Process the event publisher file, create it and deploy it
     *
     * @param deploymentFileData information about the event publisher
     * @throws org.apache.axis2.deployment.DeploymentException for any errors
     */
    public void deploy(DeploymentFileData deploymentFileData) throws DeploymentException {

        String path = deploymentFileData.getAbsolutePath();
        if (!deployedEventPublisherFilePaths.contains(path)) {
            try {
                processDeployment(deploymentFileData);
            } catch (Throwable e) {
                log.error("Cannot deploy event publisher : " + deploymentFileData.getName(), e);
                throw new DeploymentException("Event publisher file " + deploymentFileData.getName() + " is not deployed ", e);
            }
        } else {
            deployedEventPublisherFilePaths.remove(path);
        }
    }

    private OMElement getEventPublisherOMElement(File eventPublisherFile)
            throws DeploymentException {
        String fileName = eventPublisherFile.getName();
        OMElement eventPublisherElement;
        BufferedInputStream inputStream = null;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(eventPublisherFile));
            XMLStreamReader parser = XMLInputFactory.newInstance().
                    createXMLStreamReader(inputStream);
            StAXOMBuilder builder = new StAXOMBuilder(parser);
            eventPublisherElement = builder.getDocumentElement();
            eventPublisherElement.build();

        } catch (FileNotFoundException e) {
            String errorMessage = " file cannot be found with name : " + fileName;
            log.error(errorMessage, e);
            throw new DeploymentException(errorMessage, e);
        } catch (XMLStreamException e) {
            String errorMessage = "Invalid XML for file " + eventPublisherFile.getName();
            log.error(errorMessage, e);
            throw new DeploymentException(errorMessage, e);
        } catch (OMException e) {
            String errorMessage = "XML tags are not properly closed in " + fileName;
            log.error(errorMessage, e);
            throw new DeploymentException(errorMessage, e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                String errorMessage = "Can not close the input stream";
                log.error(errorMessage, e);
            }
        }
        return eventPublisherElement;
    }

    public void setExtension(String extension) {

    }

    /**
     * Removing already deployed event publisher configuration
     *
     * @param filePath the path to the event publisher artifact to be removed
     * @throws org.apache.axis2.deployment.DeploymentException
     */
    public void undeploy(String filePath) throws DeploymentException {


        if (!undeployedEventPublisherFilePaths.contains(filePath)) {
            try {
                processUndeployment(filePath);
            } catch (Throwable e) {
                log.error("Cannot undeploy event publisher : " + new File(filePath).getName(), e);
                throw new DeploymentException("Event publisher file " + new File(filePath).getName() + " is not undeployed properly", e);
            }
        } else {
            undeployedEventPublisherFilePaths.remove(filePath);
        }

    }

    public void processDeployment(DeploymentFileData deploymentFileData)
            throws DeploymentException, EventPublisherConfigurationException {

        File eventPublisherFile = deploymentFileData.getFile();
        boolean isEditable = !eventPublisherFile.getAbsolutePath().contains(File.separator + "carbonapps" + File.separator);
        CarbonEventPublisherService carbonEventPublisherService = EventPublisherServiceValueHolder.getCarbonEventPublisherService();
        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        String eventPublisherName = "";
        carbonEventPublisherService.getTenantSpecificDeployerMap().put(tenantId, this);

        if (!carbonEventPublisherService.isEventPublisherFileAlreadyExist(eventPublisherFile.getName(), tenantId)) {
            try {
                OMElement eventPublisherOMElement = getEventPublisherOMElement(eventPublisherFile);
                if (!(eventPublisherOMElement.getQName().getLocalPart()).equals(EventPublisherConstants.EF_ELEMENT_ROOT_ELEMENT)) {
                    throw new EventPublisherConfigurationException("Wrong event publisher configuration file, Invalid root element " + eventPublisherOMElement.getQName() + " in " + eventPublisherFile.getName());
                }

                boolean isEncrypted = EventPublisherConfigurationHelper.validateEncryptedProperties(eventPublisherOMElement);

                if (isEditable && !isEncrypted) {
                    String fileName = eventPublisherFile.getName();
                    EventPublisherConfigurationFilesystemInvoker.delete(fileName);
                    EventPublisherConfigurationFilesystemInvoker.encryptAndSave(eventPublisherOMElement, fileName);
                    return;
                }

                EventPublisherConfigurationHelper.validateEventPublisherConfiguration(eventPublisherOMElement);
                String mappingType = EventPublisherConfigurationHelper.getOutputMappingType(eventPublisherOMElement);
                if (mappingType != null) {
                    mappingType = mappingType.toLowerCase();
                    EventPublisherConfiguration eventPublisherConfiguration = EventPublisherConfigurationBuilder.getEventPublisherConfiguration(eventPublisherOMElement, mappingType, isEditable, tenantId);
                    eventPublisherName = eventPublisherConfiguration.getEventPublisherName();
                    String type = eventPublisherConfiguration.getToAdapterConfiguration().getType();
                    if (!EventPublisherServiceValueHolder.getOutputEventAdapterTypes().contains(type)) {
                        throw new EventPublisherValidationException("Event Adapter with type: " + type + " does not exist", type);
                    }

                    if (!carbonEventPublisherService.isEventPublisherAlreadyExists(tenantId, eventPublisherName)) {
                        carbonEventPublisherService.addEventPublisherConfiguration(eventPublisherConfiguration);
                        carbonEventPublisherService.addEventPublisherConfigurationFile(createEventPublisherConfigurationFile(eventPublisherName,
                                deploymentFileData.getFile(), EventPublisherConfigurationFile.Status.DEPLOYED, tenantId, null, null), tenantId);
                        log.info("Event Publisher configuration successfully deployed and in active state : " + eventPublisherName);
                    } else {
                        throw new EventPublisherConfigurationException("Event Publisher not deployed and in inactive state," +
                                " since there is a event publisher registered with the same name in this tenant :" + eventPublisherFile.getName());
                    }
                } else {
                    throw new EventPublisherConfigurationException("Event Publisher not deployed and in inactive state, " +
                            "since it does not contain a proper mapping type : " + eventPublisherFile.getName());
                }
            } catch (EventPublisherConfigurationException ex) {
                if (isEditable) {
                    log.error("Error, Event Publisher not deployed and in inactive state, " + ex.getMessage(), ex);
                    carbonEventPublisherService.addEventPublisherConfigurationFile(createEventPublisherConfigurationFile(eventPublisherName, deploymentFileData.getFile(),
                            EventPublisherConfigurationFile.Status.ERROR, tenantId, "Exception when deploying event publisher configuration file:\n" + ex.getMessage(), null), tenantId);
                }
                throw new EventPublisherConfigurationException(ex.getMessage(), ex);
            } catch (EventPublisherValidationException ex) {
                carbonEventPublisherService.addEventPublisherConfigurationFile(createEventPublisherConfigurationFile(eventPublisherName, deploymentFileData.getFile(),
                        EventPublisherConfigurationFile.Status.WAITING_FOR_DEPENDENCY, tenantId, ex.getMessage(), ex.getDependency()), tenantId);
                log.info("Event Publisher deployment held back and in inactive state : " + eventPublisherFile.getName() + ", waiting for Output Event Adapter dependency : " + ex.getDependency());
            } catch (EventPublisherStreamValidationException e) {
                carbonEventPublisherService.addEventPublisherConfigurationFile(createEventPublisherConfigurationFile(eventPublisherName, deploymentFileData.getFile(),
                        EventPublisherConfigurationFile.Status.WAITING_FOR_STREAM_DEPENDENCY, tenantId, e.getMessage(), e.getDependency()), tenantId);
                log.info("Event Publisher deployment held back and in inactive state :" + eventPublisherFile.getName() + ", Stream validation exception : " + e.getMessage());
            } catch (Throwable e) {
                if (isEditable) {
                    log.error("Event Publisher not deployed, invalid configuration found at " + eventPublisherFile.getName() + ", and in inactive state, " + e.getMessage(), e);
                    carbonEventPublisherService.addEventPublisherConfigurationFile(createEventPublisherConfigurationFile(eventPublisherName,
                            deploymentFileData.getFile(), EventPublisherConfigurationFile.Status.ERROR, tenantId, "Deployment exception: " + e.getMessage(), null), tenantId);
                }
                throw new EventPublisherConfigurationException(e);
            }
        } else {
            // If there is an existing event publisher definition found, this will quietly log the message and return
            // as it won't be an issue.
            log.debug("Event Publisher " + eventPublisherFile.getName() +
                    " is already registered with this tenant (" + tenantId + ")");
        }
    }

    public void processUndeployment(String filePath) {

        String fileName = new File(filePath).getName();
        log.info("Event Publisher undeployed successfully : " + fileName);
        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        CarbonEventPublisherService carbonEventPublisherService = EventPublisherServiceValueHolder.getCarbonEventPublisherService();
        carbonEventPublisherService.removeEventPublisherConfigurationFile(fileName, tenantId);
    }

    public void setDirectory(String directory) {

    }

    public void executeManualDeployment(String filePath)
            throws DeploymentException, EventPublisherConfigurationException {
        processDeployment(new DeploymentFileData(new File(filePath)));
    }

    public void executeManualUndeployment(String filePath) {
        processUndeployment(filePath);
    }

    private EventPublisherConfigurationFile createEventPublisherConfigurationFile(
            String eventPublisherName,
            File file,
            EventPublisherConfigurationFile.Status status,
            int tenantId,
            String deploymentStatusMessage,
            String dependency) {
        EventPublisherConfigurationFile eventPublisherConfigurationFile = new EventPublisherConfigurationFile();
        eventPublisherConfigurationFile.setFileName(file.getName());
        eventPublisherConfigurationFile.setFilePath(file.getAbsolutePath());
        eventPublisherConfigurationFile.setEventPublisherName(eventPublisherName);
        eventPublisherConfigurationFile.setStatus(status);
        eventPublisherConfigurationFile.setDependency(dependency);
        eventPublisherConfigurationFile.setDeploymentStatusMessage(deploymentStatusMessage);
        eventPublisherConfigurationFile.setTenantId(tenantId);

        return eventPublisherConfigurationFile;
    }

    public Set<String> getDeployedEventPublisherFilePaths() {
        return deployedEventPublisherFilePaths;
    }

    public Set<String> getUndeployedEventPublisherFilePaths() {
        return undeployedEventPublisherFilePaths;
    }

}


