/**
 * Copyright (c) 2007-2012 EBM WebSourcing, 2012-2018 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.bc.ftp.service;

import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;

import javax.activation.DataHandler;
import javax.jbi.messaging.MessagingException;

import org.ow2.petals.bc.ftp.FTPConstants;
import org.ow2.petals.bc.ftp.FTPUtil;
import org.ow2.petals.bc.ftp.FtpProvideExtFlowStepBeginLogData;
import org.ow2.petals.bc.ftp.FtpProvideExtFlowStepEndLogData;
import org.ow2.petals.bc.ftp.MissingElementException;
import org.ow2.petals.bc.ftp.connection.WrappedFTPClient;
import org.ow2.petals.commons.log.FlowAttributes;
import org.ow2.petals.commons.log.Level;
import org.ow2.petals.commons.log.PetalsExecutionContext;
import org.ow2.petals.component.framework.api.Message;
import org.ow2.petals.component.framework.api.Message.MEPConstants;
import org.ow2.petals.component.framework.api.message.Exchange;
import org.ow2.petals.component.framework.logger.ProvideExtFlowStepFailureLogData;
import org.ow2.petals.component.framework.util.MtomUtil;
import org.ow2.petals.component.framework.util.MtomUtil.MtomMapping;
import org.ow2.petals.component.framework.util.SourceUtil;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.ebmwebsourcing.easycommons.lang.StringHelper;
import com.ebmwebsourcing.easycommons.uuid.SimpleUUIDGenerator;
import com.ebmwebsourcing.easycommons.xml.XMLHelper;

/**
 * The FTP JBI listener. Writes a file to FTP server on incoming JBI message.
 * 
 * @author Mathieu CARROLLE - EBM WebSourcing
 * @author Adrien Louis - EBM WebSourcing
 * @author Roland Naudin - EBM WebSourcing
 */
public class FTPService {

    /**
     * An UUID generator.
     */
    private final SimpleUUIDGenerator simpleUUIDGenerator = new SimpleUUIDGenerator();

    private final Logger logger;

    public FTPService(final Logger logger) {
        super();
        this.logger = logger;
    }

    /***
     * FTP DEL Operation <br>
     * 
     * @param exchange
     * @param inMessage
     * @param client
     * @throws MessagingException
     * @throws MissingElementException
     */
    public void processDel(final Exchange exchange, final Document inMessage, final WrappedFTPClient client)
            throws MessagingException, MissingElementException {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.fine("JBI exchange received - Process DEL operation");
        }

        try {
            // Checks the MEP of the incoming message
            if (!exchange.isInOnlyPattern()) {
                throw new MessagingException(FTPUtil.getOperationValidMep(FTPConstants.DEL_OPERATION,
                        Message.MEPConstants.IN_ONLY_PATTERN));
            }
            // retrieve the filename to get
            final Node filePatternNode = XMLHelper.findChild(inMessage.getFirstChild(), null,
                    FTPConstants.FTP_FILENAME, false);
            final String filePattern;
            if (filePatternNode != null) {
                filePattern = filePatternNode.getTextContent();
            } else {
                throw new MissingElementException(FTPConstants.FTP_FILENAME);
            }

            if (filePattern != null) {

                final String newFlowStepId = this.simpleUUIDGenerator.getNewID();
                final FlowAttributes flowAttributes = PetalsExecutionContext.getFlowAttributes();
                final String flowInstanceId;
                final String flowStepId;
                if (flowAttributes != null) {
                    flowInstanceId = flowAttributes.getFlowInstanceId();
                    flowStepId = flowAttributes.getFlowStepId();
                } else {
                    flowInstanceId = null;
                    flowStepId = null;
                }
                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepBeginLogData(flowInstanceId, flowStepId,
                        newFlowStepId, client.getConnectionInfo().getDirectory(), "(" + filePattern + ")"));

                try {
                    // do the FTP DEL operation
                    client.del(filePattern);

                    this.logger
                            .log(Level.MONIT, "", new FtpProvideExtFlowStepEndLogData(flowInstanceId, newFlowStepId));

                } catch (final IOException e) {
                    final String errorBaseMsg = "Can not delete file on FTP server ["
                            + client.getConnectionInfo().getServer() + "]";
                    final StringBuilder errorSB = new StringBuilder(errorBaseMsg);
                    errorSB.append(" : IOException : ");
                    errorSB.append(e.getMessage());
                    this.logger.log(Level.MONIT, "", new ProvideExtFlowStepFailureLogData(flowInstanceId,
                            newFlowStepId, errorSB.toString()));
                    throw new MessagingException(errorBaseMsg, e);
                }
            }
        } finally {
            if (this.logger.isLoggable(Level.FINE)) {
                this.logger.fine("DEL operation ended");
            }
        }
    }

    /**
     * FTP DIR operation. <br>
     * a Fault is returned for bad request or IO problems
     * 
     * @param exchange
     *            the message exchange, MUST be non null
     * @throws MessagingException
     *             bad MEP
     */

    public void processDir(final Exchange exchange, final WrappedFTPClient client) throws MessagingException {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.fine("JBI exchange received - Process DIR operation");
        }
        
        try {
            if (!exchange.isInOutPattern()) {
                throw new MessagingException(FTPUtil.getOperationValidMep(FTPConstants.DIR_OPERATION,
                        MEPConstants.IN_OUT_PATTERN));
            }

            final String newFlowStepId = this.simpleUUIDGenerator.getNewID();
            final FlowAttributes flowAttributes = PetalsExecutionContext.getFlowAttributes();
            final String flowInstanceId;
            final String flowStepId;
            if (flowAttributes != null) {
                flowInstanceId = flowAttributes.getFlowInstanceId();
                flowStepId = flowAttributes.getFlowStepId();
            } else {
                flowInstanceId = null;
                flowStepId = null;
            }
            this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepBeginLogData(flowInstanceId, flowStepId,
                    newFlowStepId, client.getConnectionInfo().getDirectory()));

            try {
                // do the FTP DIR operation
                final List<String> fileNames = client.listFolderContent();

                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepEndLogData(flowInstanceId, newFlowStepId));

                // generate filename list
                final String result = FTPUtil.generateFileNameList(fileNames, exchange.getOperation());
                exchange.getOutMessage().setContent(SourceUtil.createSource(result));

            } catch (final IOException e) {
                final String errorMsg = "Can not list files on FTP server [" + client.getConnectionInfo().getServer()
                        + "]. Cause : " + e.getMessage();
                this.logger.log(Level.MONIT, "", new ProvideExtFlowStepFailureLogData(flowInstanceId, newFlowStepId,
                        errorMsg));
                FTPUtil.setIOFaultOnExchange(exchange, errorMsg);
            }
        } finally {
            if (this.logger.isLoggable(Level.FINE)) {
                this.logger.fine("DIR operation ended");
            }
        }
    }

    /**
     * 
     * <p>FTP GET operation</p>
     * <p>get the file as a source message a Fault is returned for bad request or IO problems</p>
     * 
     * @param exchange
     *            the message exchange, MUST be non null
     * @param inMessage
     * @param client
     * @throws MessagingException
     *             bad MEP
     */
    public void processGet(final Exchange exchange, final Document inMessage, final WrappedFTPClient client)
            throws MessagingException {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.fine("JBI exchange received - Process GET operation");
        }
        
        try {
            // Checks the MEP of the incoming message
            if (!exchange.isInOutPattern()) {
                throw new MessagingException(FTPUtil.getOperationValidMep(FTPConstants.GET_OPERATION,
                        Message.MEPConstants.IN_OUT_PATTERN));
            }
            // retrieve the filename to get
            final Node filePatternNode = XMLHelper.findChild(inMessage.getFirstChild(), null,
                    FTPConstants.FTP_FILENAME, false);
            final String filePattern;
            if (filePatternNode != null) {
                filePattern = filePatternNode.getTextContent();
            } else {
                filePattern = null;
            }

            if (StringHelper.isNullOrEmpty(filePattern)) {
                FTPUtil.setMissingElementFaultOnExchange(exchange, FTPConstants.FTP_FILENAME);
            } else {
                final String newFlowStepId = this.simpleUUIDGenerator.getNewID();
                final FlowAttributes flowAttributes = PetalsExecutionContext.getFlowAttributes();
                final String flowInstanceId;
                final String flowStepId;
                if (flowAttributes != null) {
                    flowInstanceId = flowAttributes.getFlowInstanceId();
                    flowStepId = flowAttributes.getFlowStepId();
                } else {
                    flowInstanceId = null;
                    flowStepId = null;
                }
                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepBeginLogData(flowInstanceId, flowStepId,
                        newFlowStepId, client.getConnectionInfo().getDirectory(), "(" + filePattern + ")"));

                try {
                    // do the FTP GET operation
                    final Document doc = client.get(filePattern);
                    if (client.getConnectionInfo().getDeleteProcessedFile()) {
                        // TODO: Perhaps should we log 2 MONIT traces, one for each interaction with FTP server ?
                        client.del(filePattern);
                    }

                    this.logger
                            .log(Level.MONIT, "", new FtpProvideExtFlowStepEndLogData(flowInstanceId, newFlowStepId));

                    final Element rootElt = doc.createElementNS(exchange.getOperation().getNamespaceURI(),
                            "ver:getResponse");
                    rootElt.appendChild(doc.getDocumentElement());
                    doc.appendChild(rootElt);
                    exchange.setOutMessageContent(doc);

                } catch (final IOException e) {
                    final String errorMsg = "Can not get file [" + filePattern + "] on FTP server ["
                            + client.getConnectionInfo().getServer() + "]. Cause : " + e.getMessage();
                    this.logger.log(Level.MONIT, "", new ProvideExtFlowStepFailureLogData(flowInstanceId,
                            newFlowStepId, errorMsg));
                    FTPUtil.setIOFaultOnExchange(exchange, errorMsg);
                }
            }
        } finally {
            if (this.logger.isLoggable(Level.FINE)) {
                this.logger.fine("GET operation ended");
            }
        }
    }

    /**
     * 
     * <p>FTP GET operation</p>
     * <p>get the file as a source message a Fault is returned for bad request or IO problems</p>
     * 
     * @param exchange
     *            the message exchange, MUST be non null
     * @param inMessage
     * @param client
     * @throws MessagingException
     *             bad MEP
     */
    public void processGetAsAttachment(final Exchange exchange, final Document inMessage, final WrappedFTPClient client)
            throws MessagingException {
        this.logger.fine("JBI exchange received - Process GET AS ATTACHMENT operation");

        try {
            // Checks the MEP of the incoming message
            if (!exchange.isInOutPattern()) {
                throw new MessagingException(FTPUtil.getOperationValidMep(FTPConstants.GET_ATTACHMENT_OPERATION,
                        Message.MEPConstants.IN_OUT_PATTERN));
            }

            // retrieve the filename to get
            final Node filePatternNode = XMLHelper.findChild(inMessage.getFirstChild(), null,
                    FTPConstants.FTP_FILENAME, true);
            final String filePattern;
            if (filePatternNode != null) {
                filePattern = filePatternNode.getTextContent();
            } else {
                filePattern = null;
            }
            if (StringHelper.isNullOrEmpty(filePattern)) {
                FTPUtil.setMissingElementFaultOnExchange(exchange, FTPConstants.FTP_FILENAME);
            } else {
                final String newFlowStepId = this.simpleUUIDGenerator.getNewID();
                final FlowAttributes flowAttributes = PetalsExecutionContext.getFlowAttributes();
                final String flowInstanceId;
                final String flowStepId;
                if (flowAttributes != null) {
                    flowInstanceId = flowAttributes.getFlowInstanceId();
                    flowStepId = flowAttributes.getFlowStepId();
                } else {
                    flowInstanceId = null;
                    flowStepId = null;
                }
                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepBeginLogData(flowInstanceId, flowStepId,
                        newFlowStepId, client.getConnectionInfo().getDirectory(), "(" + filePattern + ")"));

                try {
                    // do the FTP GET operation
                    final DataHandler file = client.getFileAsAttachment(filePattern);
                    if (client.getConnectionInfo().getDeleteProcessedFile()) {
                        // TODO: Perhaps should we log 2 MONIT traces, one for each interaction with FTP server ?
                        client.del(file.getName());
                    }

                    this.logger
                            .log(Level.MONIT, "", new FtpProvideExtFlowStepEndLogData(flowInstanceId, newFlowStepId));

                    exchange.setOutMessageAttachment(file.getName(), file);
                    final Document outDoc = FTPUtil.generateMTOMResponse(file.getName(), exchange.getOperation());
                    exchange.setOutMessageContent(outDoc);

                } catch (final IOException e) {
                    final String errorMsg = "Can not get file [" + filePattern + "] on FTP server ["
                            + client.getConnectionInfo().getServer() + "]. Cause : " + e.getMessage();
                    this.logger.log(Level.MONIT, "", new ProvideExtFlowStepFailureLogData(flowInstanceId,
                            newFlowStepId, errorMsg));
                    FTPUtil.setIOFaultOnExchange(exchange, errorMsg);
                }
            }
        } finally {
            this.logger.fine("GET AS ATTACHMENT operation ended");
        }
    }

    private final String filenamesAsPatterns(final Collection<String> filenames) {
        assert !filenames.isEmpty();
        final StringBuffer sb = new StringBuffer("(");
        for (final String filename : filenames) {
            sb.append(filename);
            sb.append("|");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append(")");
        return sb.toString();
    }

    /**
     * <p>FTP MGET operation</p>
     * <p>All files are getted as attachments</p>
     * 
     * @param exchange
     * @param inMessage
     * @param client
     * @throws MessagingException
     */
    public void processMGet(final Exchange exchange, final Document inMessage, final WrappedFTPClient client)
            throws MessagingException {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.fine("JBI exchange received - Process MGET operation");
        }

        try {
            if (!exchange.isInOutPattern()) {
                throw new MessagingException(FTPUtil.getOperationValidMep(FTPConstants.MGET_OPERATION,
                        MEPConstants.IN_OUT_PATTERN));
            }
            // retrieve filenames to get
            final NodeList list = inMessage.getFirstChild().getChildNodes();
            final List<String> filenames = new LinkedList<String>();
            for (int index = 0; index < list.getLength(); index++) {
                if (FTPConstants.FTP_FILENAME.equalsIgnoreCase(list.item(index).getLocalName())) {
                    filenames.add(list.item(index).getTextContent());
                }
            }
            if (filenames.size() == 0) {
                FTPUtil.setMissingElementFaultOnExchange(exchange, FTPConstants.FTP_FILENAME);
            } else {
                final String newFlowStepId = this.simpleUUIDGenerator.getNewID();
                final FlowAttributes flowAttributes = PetalsExecutionContext.getFlowAttributes();
                final String flowInstanceId;
                final String flowStepId;
                if (flowAttributes != null) {
                    flowInstanceId = flowAttributes.getFlowInstanceId();
                    flowStepId = flowAttributes.getFlowStepId();
                } else {
                    flowInstanceId = null;
                    flowStepId = null;
                }
                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepBeginLogData(flowInstanceId, flowStepId,
                        newFlowStepId, client.getConnectionInfo().getDirectory(), filenamesAsPatterns(filenames)));

                try {
                    // do the FTP MGET operation
                    final Map<String, DataHandler> files = client.mGet(filenames);
                    // add attachments on the OUT message
                    for (Entry<String, DataHandler> entry : files.entrySet()) {
                        exchange.getOutMessage().addAttachment(entry.getKey(), entry.getValue());
                    }
                    final List<String> retrievedFilenames = new LinkedList<String>(files.keySet());
                    final Document outDoc = FTPUtil.generateMTOMListResponse(retrievedFilenames,
                            exchange.getOperation());
                    exchange.setOutMessageContent(outDoc);
                    if (client.getConnectionInfo().getDeleteProcessedFile()) {
                        client.mDel(retrievedFilenames);
                    }
                    this.logger
                            .log(Level.MONIT, "", new FtpProvideExtFlowStepEndLogData(flowInstanceId, newFlowStepId));

                } catch (final IOException e) {
                    final String errorMsg = "Can not get files [" + filenames + "] on FTP server ["
                            + client.getConnectionInfo().getServer() + "]. Cause : " + e.getMessage();
                    this.logger.log(Level.MONIT, "", new ProvideExtFlowStepFailureLogData(flowInstanceId,
                            newFlowStepId, errorMsg));
                    FTPUtil.setIOFaultOnExchange(exchange, errorMsg);
                }
            }
        } finally {
            if (this.logger.isLoggable(Level.FINE)) {
                this.logger.fine("MGET operation ended");
            }
        }
    }

    /**
     * <p>Put attachments on the ftp server</p>
     *
     * TODO an FTP connection is done for each file...
     * 
     * @param exchange
     * @param inputDocument
     * @param client
     * @throws MessagingException
     */
    public void processMPut(final Exchange exchange, final Document inputDocument, final WrappedFTPClient client)
            throws MessagingException {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.fine("JBI exchange received - Process MPUT operation");
        }

        try {
            // Checks the MEP of the incoming message
            if (!exchange.isInOnlyPattern()) {
                throw new MessagingException(FTPUtil.getOperationValidMep(FTPConstants.MPUT_OPERATION,
                        MEPConstants.IN_ONLY_PATTERN));
            }

            // Check if the message has attachements
            if (exchange.getInMessageAttachments().isEmpty()) {
                final StringBuilder errorSB = new StringBuilder();
                errorSB.append("One or more attachments expected.");
                throw new MessagingException(errorSB.toString());
            }

            final String newFlowStepId = this.simpleUUIDGenerator.getNewID();
            final FlowAttributes flowAttributes = PetalsExecutionContext.getFlowAttributes();
            final String flowInstanceId;
            final String flowStepId;
            if (flowAttributes != null) {
                flowInstanceId = flowAttributes.getFlowInstanceId();
                flowStepId = flowAttributes.getFlowStepId();
            } else {
                flowInstanceId = null;
                flowStepId = null;
            }
            try {
                // rebuild a hashmap containing files and their names
                final MtomMapping mapping = MtomUtil.getMtomMapping(exchange, inputDocument.getDocumentElement());
                final Map<String, DataHandler> attachments = mapping.getContentIdToDataHandler();
                if (this.logger.isLoggable(Level.FINE)) {
                    this.logger.fine(mapping.toString());
                }
                final String filenamesAsPatterns = filenamesAsPatterns(attachments.keySet());

                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepBeginLogData(flowInstanceId, flowStepId,
                        newFlowStepId, client.getConnectionInfo().getDirectory(), filenamesAsPatterns));

                client.mPut(attachments);

                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepEndLogData(flowInstanceId, newFlowStepId));

            } catch (final IOException e) {
                final String errorBaseMsg = "Can not put files on FTP server [" + client.getConnectionInfo().getServer()
                        + "]";
                final StringBuilder errorSB = new StringBuilder(errorBaseMsg);
                errorSB.append(" : IOException : ");
                errorSB.append(e.getMessage());
                this.logger.log(Level.MONIT, "", new ProvideExtFlowStepFailureLogData(flowInstanceId, newFlowStepId,
                        errorSB.toString()));
                throw new MessagingException(errorBaseMsg, e);
            }
        } finally {
            this.logger.fine("MPUT operation ended");
        }
    }

    /**
     * <p>Put the source on the ftp server with the given filename a Fault is returned on IO problems</p>
     *
     * TODO the content to send is a String, which is not optimal.
     * 
     * @param exchange
     * @param inMessage
     * @param client
     * @throws MessagingException
     * @throws MissingElementException
     */
    public void processPut(final Exchange exchange, final Document inMessage, final WrappedFTPClient client)
            throws MessagingException, MissingElementException {

        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.fine("JBI exchange received - Process PUT operation");
        }

        try {
            if (exchange == null) {
                throw new MessagingException("exchange parameter is null");
            }

            // Checks the MEP of the incoming message
            if (!exchange.isInOnlyPattern()) {
                throw new MessagingException(FTPUtil.getOperationValidMep(FTPConstants.PUT_OPERATION,
                        MEPConstants.IN_ONLY_PATTERN));
            }
            // Check the root node
            final Node root = inMessage.getFirstChild();
            if (root == null || !FTPConstants.PUT_OPERATION.equalsIgnoreCase(root.getLocalName())) {
                throw new MissingElementException(FTPConstants.PUT_OPERATION);
            }

            final Node fileNameNode = XMLHelper.findChild(root, null, FTPConstants.FTP_FILENAME, false);
            if (fileNameNode == null || !FTPConstants.FTP_FILENAME.equalsIgnoreCase(fileNameNode.getLocalName())) {
                throw new MissingElementException(FTPConstants.FTP_FILENAME);
            }
            final Node bodyNode = XMLHelper.findChild(root, null, FTPConstants.BODY, false);
            if (bodyNode == null) {
                throw new MissingElementException(FTPConstants.BODY);
            }

            final String newFlowStepId = this.simpleUUIDGenerator.getNewID();
            final FlowAttributes flowAttributes = PetalsExecutionContext.getFlowAttributes();
            final String flowInstanceId;
            final String flowStepId;
            if (flowAttributes != null) {
                flowInstanceId = flowAttributes.getFlowInstanceId();
                flowStepId = flowAttributes.getFlowStepId();
            } else {
                flowInstanceId = null;
                flowStepId = null;
            }

            try {
                final String body = XMLHelper.toString(bodyNode.getChildNodes());

                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepBeginLogData(flowInstanceId, flowStepId,
                        newFlowStepId, client.getConnectionInfo().getDirectory(), fileNameNode.getTextContent()));

                client.putString(fileNameNode.getTextContent(), body);

                this.logger.log(Level.MONIT, "", new FtpProvideExtFlowStepEndLogData(flowInstanceId, newFlowStepId));

            } catch (final DOMException | IOException e) {
                final String errorBaseMsg = "Can not put XML content on FTP server ["
                        + client.getConnectionInfo().getServer() + "]";
                final StringBuilder errorSB = new StringBuilder(errorBaseMsg);
                errorSB.append(" : ").append(e.getClass().getName()).append(" : ");
                errorSB.append(e.getMessage());
                this.logger.log(Level.MONIT, "", new ProvideExtFlowStepFailureLogData(flowInstanceId, newFlowStepId,
                        errorSB.toString()));
                throw new MessagingException(errorBaseMsg, e);
            }
        } finally {
            if (this.logger.isLoggable(Level.FINE)) {
                this.logger.fine("PUT operation ended");
            }
        }
    }
}
