/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (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.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * 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 com.queplix.core.modules.inbox.actions;

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.integrator.security.LogonSession;
import com.queplix.core.modules.attachment.ejb.AttachmentManagerLocal;
import com.queplix.core.modules.attachment.ejb.AttachmentManagerLocalHome;
import com.queplix.core.modules.eql.ejb.AsyncMDB;
import com.queplix.core.modules.inbox.Account;
import com.queplix.core.modules.inbox.InboxHelper;
import com.queplix.core.modules.inbox.InboxMessage;
import com.queplix.core.modules.inbox.ejb.InboxManagerLocal;
import com.queplix.core.modules.inbox.ejb.InboxManagerLocalHome;
import com.queplix.core.modules.inbox.utils.DefaultMessageFilter;
import com.queplix.core.modules.inbox.utils.InboxMsgBuilder;
import com.queplix.core.modules.inbox.utils.InboxPropertyFactory;
import com.queplix.core.modules.inbox.utils.InboxProvider;
import com.queplix.core.modules.inbox.utils.MailFilter;
import com.queplix.core.modules.inbox.utils.ResultSet;
import com.queplix.core.modules.inbox.utils.log.SystemLogPublisher;
import com.queplix.core.modules.mail.Attachment;
import com.queplix.core.modules.services.XAAction;
import com.queplix.core.modules.services.XAActionContext;
import com.queplix.core.utils.DateHelper;
import com.queplix.core.utils.JNDINames;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.async.ASyncObject;
import com.queplix.core.utils.async.ASyncRequest;
import com.queplix.core.utils.async.JMSClient;
import com.queplix.core.utils.cache.CacheObjectManager;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;

import javax.mail.Message;
import javax.mail.MessagingException;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
 * The service scans incomming emails.
 *
 * @author Konstantin Mironov
 * @since 8 Dec 2006
 */
public class InboxAction extends XAAction {

    // ------------------------------------------------------- Fields

    // Service and log names.
    private String serviceName;
    private String logName;

    // Required service parameters:
    private Account account;
    private Long accountID;
    // == or ==
    private InboxMessage inboxMessage;

    // Optional service parameters.
    private Integer batchSize;
    private Boolean updateAccount;

    private LogonSession ls;
    private SystemLogPublisher publisher;

    // Async client.
    private AsyncClient asyncClient;

    // AttachmentManager EJB local interface
    private AttachmentManagerLocal attachMgr;

    private static final AbstractLogger logger = Log.getLog(InboxAction.class);

    // ------------------------------------------------------- API methods

    /*
     * No javadoc
     * @see Action#perform
     */

    public java.io.Serializable perform() {

        long time = System.currentTimeMillis();
        boolean error = false;

        // Initialization.
        init();

        try {
            if(inboxMessage != null) {
                // Performing concrete InboxMessage.
                __perform(inboxMessage);
            } else {
                // Performing all set of messages.
                __perform();
            }

        } catch (MessagingException me) {
            // Write error log.
            error = true;

            String msg = "Messaging exception: " + me.getMessage();
            publisher.ERROR(msg, accountID);
            logger.ERROR(logName + msg, me);

        } catch (Throwable t) {
            // Write error log.
            error = true;

            String msg = "Unknown exception: " + t.getMessage();
            publisher.ERROR(msg, accountID);
            logger.ERROR(logName + msg, t);

        } finally {

            // The end.
            time = (System.currentTimeMillis() - time) / 1000;
            if(error) {

                // If check failed, mark the account as invalid.
                if(Boolean.TRUE.equals(updateAccount)) {
                    account.setValidFlag(Boolean.FALSE);
                }

                String msg = "Service '" + serviceName
                        + "' completed with errors. " +
                        "Time = " + time + " sec.";
                publisher.WARN(msg, accountID);
                logger.WARN(logName + msg);

            } else {

                // If checked successfully, mark the account as valid.
                if(Boolean.TRUE.equals(updateAccount)) {
                    account.setValidFlag(Boolean.TRUE);
                }

                String msg = "Service '" + serviceName + "' finished. Time = "
                        + time + " sec.";
                publisher.INFO(msg, accountID);
                logger.INFO(logName + msg);
            } // if (error)
        } // try

        return null;

    } // perform() : java.io.Serializable

    // ------------------------------------------------------- Private methods

    /**
     * Performs set of messages.
     *
     * @throws MessagingException the message has wrong format
     * @throws IOException        saving error
     */
    private void __perform() throws MessagingException, IOException {

        // Initialization.
        int processedMessages = 0;
        Date msgMaxServerDate = null;
        boolean deleteMailFlag = Boolean.TRUE.equals(
                account.getDeleteMailFlag());
        boolean markEmailAsSeenFlag = Boolean.TRUE.equals(
                account.getMarkEmailAsSeenFlag());
        String lastMessageUID = null;
        String lastMessageDigest = null;

        // Init Inbox provider.
        Class providerClazz = InboxPropertyFactory.getInstance()
                .getInboxProviderClass(account.getProviderName());
        InboxProvider provider = (InboxProvider) InboxPropertyFactory
                .getInstance().getInstance(providerClazz, account, logName,
                publisher);

        try {
            // Opening mail session...
            provider.open();

            // Get mail ResultSet.
            DefaultMessageFilter filter = new DefaultMessageFilter(account,
                    provider);
            ResultSet rs = provider.getResultSet(filter);

            // Messages processing...
            Message message;
            while((message = rs.next()) != null) {
                int id = message.getMessageNumber();

                logger.DEBUG(logName + "Building message #" + id);
                logger.DEBUG(
                        logName + "      Subject: " + message.getSubject());

                // Init Inbox message builder.
                Class msgBuilderClazz = InboxPropertyFactory.getInstance()
                        .getMessageBuilderClass();
                InboxMsgBuilder builder = (InboxMsgBuilder) InboxPropertyFactory
                        .getInstance().
                        getInstance(msgBuilderClazz, account, logName,
                                publisher);
                builder.setInboxProvider(provider);

                // Do build InboxMessage.
                InboxMessage im;
                try {
                    // create InboxMessage object
                    im = builder.build(message);
                } catch (MessagingException me) {
                    // just report problem
                    String msg = "Cannot parse message #" + id
                            + " with subject '" +
                            message.getSubject()
                            + "' due to messaging exception: " + me
                            .getMessage();

                    publisher.ERROR(msg, accountID);
                    logger.ERROR(logName + msg, me);
                    continue;
                } // try

                // check message digest
                String lastServerMessageDigest = account.getMessageDigest();
                String messageDigest = im.getMessageDigest();
                if(lastServerMessageDigest != null && messageDigest != null) {
                    if(lastServerMessageDigest.compareTo(messageDigest) == 0) {
                        // just report problem
                        String msg = "Message #" + id
                                + " skipped - message Digect is the same as the Last Message Digect. "
                                +
                                "Message Digest: " + messageDigest +
                                ". Last Message Digest: "
                                + lastServerMessageDigest;

                        publisher.INFO(msg, accountID);
                        logger.INFO(logName + msg);

                        // Set 'Max message date'.
                        Date msgServerDate = provider.getServerDate(message);
                        if(msgServerDate != null) {
                            if(msgMaxServerDate == null || msgServerDate.after(
                                    msgMaxServerDate)) {
                                msgMaxServerDate = msgServerDate;
                            }
                        } // if (msgServerDate != null)
                        continue;
                    } // if (lastServerMessageDigest.compareTo(messageDigest) == 0)
                } // if (messageUID != null && currentUID != null)

                // Process parsed message.
                try {
                    process(id, im);
                } catch (Exception ex) {
                    // just report problem
                    String msg = "Cannot process message #" + id
                            + " with subject '" +
                            message.getSubject() + "' due to the issue: " + ex
                            .getMessage();

                    publisher.ERROR(msg, accountID);
                    logger.ERROR(logName + msg, ex);

                    // Set 'Max message date'.
                    Date msgServerDate = provider.getServerDate(message);
                    if(msgServerDate != null) {
                        if(msgMaxServerDate == null || msgServerDate.after(
                                msgMaxServerDate)) {
                            msgMaxServerDate = msgServerDate;
                        }
                    } // if (msgServerDate != null)
                    continue;
                } // try

                // Deleting PROCESSED messages.
                if(deleteMailFlag) {
                    DEBUG("Set delete flag for #" + id + " message.");
                    provider.setDeletedMsgFlag(message);

                } else if(markEmailAsSeenFlag) {
                    DEBUG("Set seen flag for #" + id + " message.");
                    provider.setSeenMsgFlag(message);
                } // if (deleteMailFlag)

                // Set 'Max message date'.
                Date msgServerDate = im.getReceiveTime();
                if(msgServerDate != null) {
                    if(msgMaxServerDate == null || msgServerDate.after(
                            msgMaxServerDate)) {
                        msgMaxServerDate = msgServerDate;
                    }
                } // if (msgServerDate != null)

                // Checking the messages process-at-once limit.
                processedMessages++;
                if(batchSize != null && processedMessages >= batchSize
                        .intValue()) {
                    break;
                }
                lastMessageUID = im.getMessageUid();
                lastMessageDigest = im.getMessageDigest();

            } // while

            String msg = "Processed " + processedMessages + " message(s).";
            if(msgMaxServerDate != null) {
                msg += " Last processed message server date: " + DateHelper
                        .formatDate(msgMaxServerDate);
            } // if (msgMaxServerDate != null)

            publisher.INFO(msg, accountID);
            logger.INFO(logName + msg);

        } finally {
            try {
                provider.close(
                        deleteMailFlag); // expunges all deleted messages (if deleteMailFlag = true)
                logger.INFO("Folder closed. Delete flag: " + deleteMailFlag);
            } catch (Exception ex) {
                logger.ERROR(
                        "Cannot close folder. Delete flag: " + deleteMailFlag,
                        ex);
            } // try

            logger.INFO("Check and set last Receive Date");

            // Update account's 'Last receive date'.
            Date accLastReceiveDate = null;
            if(account.getLastReceiveDate() != null) {
                accLastReceiveDate = account.getLastReceiveDate();
            }

            logger.DEBUG("Account 'Last Received Date': " + accLastReceiveDate);
            logger.DEBUG(
                    "Processed message 'Max Server Date': " + msgMaxServerDate);

            if(msgMaxServerDate != null) {
                if(accLastReceiveDate == null || msgMaxServerDate.after(
                        accLastReceiveDate)) {
                    account.setLastReceiveDate(msgMaxServerDate, lastMessageUID,
                            lastMessageDigest);
                } else {
                    account.setLastMessageID(lastMessageUID, lastMessageDigest);
                } // if (accLastReceiveDate == null || msgMaxServerDate.after(accLastReceiveDate))
            } // if (msgMaxServerDate != null)
        } // try

    } // __perform()

    /**
     * Performs <code>msg</code> message.
     *
     * @param im InboxMessage
     * @throws MessagingException the message has wrong format
     * @throws IOException        saving error
     */
    private void __perform(InboxMessage im)
            throws MessagingException, IOException {

        process(1, im);

    } // __perform(InboxMessage)

    /**
     * Process one email message.
     *
     * @param id           e-mail ID
     * @param inboxMessage InboxMessage
     * @throws MessagingException the message has wrong format
     * @throws IOException        saving error
     */
    private void process(int id, InboxMessage inboxMessage)
            throws MessagingException, IOException {

        inboxMessage.setAccountId(accountID);
        inboxMessage.setRoutingInfo(account.getDefaultOwner(),
                account.getDefaultWorkgroup());
        // Init.
        List attachments = inboxMessage.getAttachments();
        int size = (attachments == null) ? 0:attachments.size();
        int length = (inboxMessage.getBody() == null) ? 0
                :inboxMessage.getBody().length();
        logger.DEBUG(logName + "Body size: " + length);
        logger.DEBUG(logName + "Attachments: " + size);

        // Storing attachments.
        if(attachments != null && size > 0) {
            long processID = attachMgr.getUniqueProcessID();
            inboxMessage.setProcessId(new Long(processID));
            for(int i = 0; i < size; i++) {
                Attachment attach = (Attachment) attachments.get(i);
                attachMgr.addTempAttachment(ls,
                        processID,
                        attach.getFilename(),
                        attach.getFiletype(),
                        attach.getData());
            } // for (int i = 0; i < size; i++)

            // Reset list before making async call.
            // Large binary data may affect total system performance.
            inboxMessage.resetAttachmentList();
            inboxMessage.setIsAttachmentSaved(false);
        } // if (size > 0)

        // set HTML indicator
        if(isHTMLContent(inboxMessage.getBody())) {
            inboxMessage.setBody(
                    inboxMessage.getBody() + StringHelper.HTML_INDICATOR);
        } else if(inboxMessage.getBody() != null) {
            inboxMessage.setBody(StringHelper.text2htmlNoTag(
                    inboxMessage.getBody()) + StringHelper.HTML_INDICATOR);
        } else {
            inboxMessage.setBody(StringHelper.HTML_INDICATOR);
        } // if (isHTMLContent(inboxMessage.getBody()))

        //InboxManagerLocal inboxManager = InboxHelper.getInboxManager();
        InboxManagerLocal inboxManager
                = (InboxManagerLocal) new CacheObjectManager().getLocalObject(
                JNDINames.InboxManager, InboxManagerLocalHome.class);
        // change the reference HTML tag <a>
        inboxManager.changeReferenceHTMLATAG(ls, inboxMessage);

        // convert email body, set image loader
        if(inboxMessage.getProcessId() != null) {
            if(!inboxManager.setImageAttachmentLoader(ls, inboxMessage)) {
                logger.INFO(
                        "The image loader for the email body wasn't set. The inserted images won't be showed.");
            }
        } // if (inboxMessage.getProcessId() != null)

        // Sending the JMS request to process message on server.
        /*
        logger.DEBUG(logName + "Calling async client for message #" + id);
        AsyncAction async = new AsyncAction(account, logName);
        asyncClient.sendMessage(async, inboxMessage);
        */
        List nameList = InboxPropertyFactory.getInstance().getMailFilterNames();
        if(nameList == null) {
            throw new NullPointerException(
                    "No any MailFilter found in config!");
        } // if (nameList == null)

        int filterCount = nameList.size();
        for(int i = 0; i < filterCount; i++) {
            String name = (String) nameList.get(i);

            // Init Mail filter.
            Class clazz = InboxPropertyFactory.getInstance().getMailFilterClass(
                    name);
            MailFilter filter = (MailFilter) InboxPropertyFactory.getInstance()
                    .getInstance(clazz, account, logName, publisher);

            // Call MailFilter#filterMail
            logger.DEBUG("AsyncAction#process(): " + hashCode());
            logger.DEBUG(
                    "    try to apply filter '" + name + "', class: " + clazz);
            logger.DEBUG(
                    "         current pos = " + i + " [" + filterCount + "]");

            RuntimeException error = null;
            try {
                if(!filter.filterMail(inboxMessage)) {
                    logger.WARN("      filter '" + name
                            + "' returned false - break processing of this email message.");
                    break;
                } // if (!filter.filterMail(im))
            } catch (RuntimeException ex) {
                error = ex;
            } catch (Exception ex) {
                error = new GenericSystemException(ex);
            } // try

            if(error != null) {
                // Caught exception.
                // -> call error handler and throw error.
                String msg = "Filter '" + name + "' failed: " + error
                        .getMessage();

                //sysPublisher.ERROR(msg, new Long(account.getAccountID()));
                logger.ERROR(msg);
                logger.ERROR(error);

                try {
                    localProcessOnError(inboxMessage);
                } catch (Exception exsp) {
                    logger.ERROR("The Error filter is failed due to: " + exsp
                            .getMessage());
                    logger.ERROR(exsp);
                    return;
                } // try
                //throw error;

            } else {
                logger.DEBUG("    filter '" + name + "' applied ok");
            } // if (error != null)
        } // for (int i = 0; i < size; i++)

        // the processing of the email was done, delete tmp attachments
        inboxManager.deleteTempAttachment(ls, inboxMessage);

        // Ok.
        logger.INFO("\n=== " + logName + "Message # " + id
                + " went to process routine. ===\n");

    } // process(int, InboxMessage)

    /**
     * The method gets the initial parameters.
     */
    private void init() {
        // Get context.
        XAActionContext context = (XAActionContext) getContext();
        ls = context.getLogonSession();

        // Init system logger.
        publisher = (SystemLogPublisher) context.getLog();
        if(publisher == null) {
            publisher = new SystemLogPublisher(ls);
        } // if (publisher == null)

        // Read required parameters.
        // These parameters should be passed:
        //      1. 'account' - Account instance
        //      2. 'im' - InboxMessage instance (optional)
        account = (Account) context.getParameter("account");
        if(account == null) {
            throw new NullPointerException("Account not passed!");
        }
        accountID = new Long(account.getAccountID());
        inboxMessage = (InboxMessage) context.getParameter("im");

        // Read optional parameters.
        batchSize = context.getParamAsInt("batchSize");
        updateAccount = context.getParamAsBoolean("updateAccount");

        // Init async client.
        asyncClient = new AsyncClient();

        // Init AttachmentManager EJB.
        attachMgr = (AttachmentManagerLocal) context.getCOM().
                getLocalObject(JNDINames.AttachmentManager,
                        AttachmentManagerLocalHome.class);

        // Construct service name.
        serviceName = InboxHelper.toURI(account);
        logName = "[" + serviceName + "] ";

        // Logging...
        String msg = "Service '" + serviceName + "' started.";
        publisher.INFO(msg, accountID);
        INFO(logName + msg);
    } // init()

    private boolean localProcessOnError(InboxMessage im) {
        // Init Mail filter.
        Class errClazz = InboxPropertyFactory.getInstance()
                .getErrorMailFilterClass();
        MailFilter errFilter = (MailFilter) InboxPropertyFactory.getInstance()
                .getInstance(errClazz, account, logName, publisher);
        DEBUG("    try to apply error filter class: " + errClazz);
        return errFilter.filterMail(im);
    } // processOnError(InboxMessage)

    /**
     * The method checks - is given string .
     *
     * @param s the string to check
     * @return <b>true</b> if <br> presents
     */
    public boolean isHTMLContent(String s) {
        if(StringHelper.isEmpty(s)) {
            return false;
        }
        boolean returnValue = false;
        try {
            RE re = new RE("<html((\\s+[^>]*)|(>))", RE.MATCH_CASEINDEPENDENT);
            if(re.match(s)) {
                returnValue = true;
            } else {
                returnValue = false;
            }

            if(!returnValue) {
                RE reBr = new RE("<br((\\s+[^>]*)|(>))",
                        RE.MATCH_CASEINDEPENDENT);
                if(reBr.match(s)) {
                    returnValue = true;
                } else {
                    returnValue = false;
                }
            }
        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }

        return returnValue;
    } // isHTMLContent(String) : boolean

    // ============================================================= Inner classes

    /**
     * Inner class to make Inbox async action.
     *
     * @author Konstantin Mironov
     * @since 8 Dec 2006
     */
    private static class AsyncClient extends JMSClient {

        /**
         * Constructor
         */
        public AsyncClient() {
            super(JNDINames.JmsConnectionFactory, JNDINames.JmsAsyncQueue);
        }

        /**
         * Send message to message driven bean
         *
         * @param obj     ASyncObject object
         * @param request param for process method ASyncObject interface
         */
        private void sendMessage(ASyncObject obj, ASyncRequest request) {

            HashMap map = new HashMap();
            map.put(AsyncMDB.OBJECT_PARAM, obj);
            if(request != null) {
                map.put(AsyncMDB.REQUEST_PARAM, request);
            }
            send(map);
        } // sendMessage(ASyncObject, ASyncRequest)

    } // static class AsyncClient extends JMSClient

    /**
     * Inner class represents Inbox async action.
     *
     * @author Konstantin Mironov
     * @since 8 Dec 2006
     */
    public static class AsyncAction implements ASyncObject {

        private Account account;
        private SystemLogPublisher sysPublisher;
        private String logName;

        // Logger.
        private AbstractLogger logger = Log.getLog(getClass());

        //
        // Constructor
        //
        AsyncAction(Account account, String logName,
                    SystemLogPublisher sysPublisher) {
            this.account = account;
            this.logName = logName;
            this.sysPublisher = sysPublisher;
        } // AsyncAction(Account, String, SystemLogPublisher)

        /**
         * Make async process.
         *
         * @param request async request
         */
        public void process(ASyncRequest request) {

            logger.DEBUG("Starting AsyncAction#process(): " + hashCode());

            // Get InboxMessage from request.
            InboxMessage im = (InboxMessage) request;

            List nameList = InboxPropertyFactory.getInstance()
                    .getMailFilterNames();
            if(nameList == null) {
                throw new NullPointerException(
                        "No any MailFilter found in config!");
            } // if (nameList == null)

            int size = nameList.size();
            for(int i = 0; i < size; i++) {
                String name = (String) nameList.get(i);

                // Init Mail filter.
                Class clazz = InboxPropertyFactory.getInstance()
                        .getMailFilterClass(name);
                MailFilter filter = (MailFilter) InboxPropertyFactory
                        .getInstance().getInstance(clazz, account, logName,
                        sysPublisher);

                // Call MailFilter#filterMail
                logger.DEBUG("AsyncAction#process(): " + hashCode());
                logger.DEBUG("    try to apply filter '" + name + "', class: "
                        + clazz);
                logger.DEBUG("         current pos = " + i + " [" + size + "]");

                RuntimeException error = null;
                try {
                    if(!filter.filterMail(im)) {
                        logger.WARN("      filter '" + name
                                + "' returned false - break cycle");
                        break;
                    } // if (!filter.filterMail(im))
                } catch (RuntimeException ex) {
                    error = ex;
                } catch (Exception ex) {
                    error = new GenericSystemException(ex);
                } // try

                if(error != null) {
                    // Caught exception.
                    // -> call error handler and throw error.
                    String msg = "Filter '" + name + "' failed: " + error
                            .getMessage();

                    //sysPublisher.ERROR(msg, new Long(account.getAccountID()));
                    logger.ERROR(msg);
                    logger.ERROR(error);

                    processOnError(im);
                    throw error;

                } else {
                    logger.DEBUG("    filter '" + name + "' applied ok");
                } // if (error != null)
            } // for (int i = 0; i < size; i++)
            logger.DEBUG("AsyncAction#process() finished: " + hashCode());

        } // process(ASyncRequest)

        /**
         * Default error handler.
         * Calls error filter.
         *
         * @param im InboxMessage
         * @return boolean
         */
        private boolean processOnError(InboxMessage im) {

            // Init Mail filter.
            Class errClazz = InboxPropertyFactory.getInstance()
                    .getErrorMailFilterClass();
            MailFilter errFilter = (MailFilter) InboxPropertyFactory
                    .getInstance().getInstance(errClazz, account, logName,
                    sysPublisher);

            // Call MailFilter#filterMail
            logger.DEBUG("    try to apply error filter class: " + errClazz);
            return errFilter.filterMail(im);
        } // processOnError(InboxMessage) : boolean
    } // static class AsyncAction implements ASyncObject
} // class InboxAction extends XAAction
