/**
 * Copyright 2005-2016 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.ken.web.spring;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
import org.kuali.rice.core.framework.persistence.dao.GenericDao;
import org.kuali.rice.ken.bo.NotificationBo;
import org.kuali.rice.ken.bo.NotificationChannelBo;
import org.kuali.rice.ken.bo.NotificationChannelReviewerBo;
import org.kuali.rice.ken.bo.NotificationContentTypeBo;
import org.kuali.rice.ken.bo.NotificationPriorityBo;
import org.kuali.rice.ken.bo.NotificationProducerBo;
import org.kuali.rice.ken.bo.NotificationRecipientBo;
import org.kuali.rice.ken.bo.NotificationSenderBo;
import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument;
import org.kuali.rice.ken.exception.ErrorList;
import org.kuali.rice.ken.service.NotificationChannelService;
import org.kuali.rice.ken.service.NotificationMessageContentService;
import org.kuali.rice.ken.service.NotificationRecipientService;
import org.kuali.rice.ken.service.NotificationService;
import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
import org.kuali.rice.ken.util.NotificationConstants;
import org.kuali.rice.ken.util.Util;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.rule.GenericAttributeContent;
import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class is the controller for sending Simple notification messages via an end user interface.
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class SendNotificationMessageController extends BaseSendNotificationController {
    /** Logger for this class and subclasses */
    private static final Logger LOG = Logger
            .getLogger(SendNotificationMessageController.class);

    private static final String NONE_CHANNEL = "___NONE___";
    private static final long REASONABLE_IMMEDIATE_TIME_THRESHOLD = 1000 * 60 * 5; // <= 5 minutes is "immediate"

    /**
     * Returns whether the specified time is considered "in the future", based on some reasonable
     * threshold
     * @param time the time to test
     * @return whether the specified time is considered "in the future", based on some reasonable
     *         threshold
     */
    private boolean timeIsInTheFuture(long time) {
        boolean future = (time - System.currentTimeMillis()) > REASONABLE_IMMEDIATE_TIME_THRESHOLD;
        LOG.info("Time: " + new Date(time) + " is in the future? " + future);
        return future;
    }

    /**
     * Returns whether the specified Notification can be reasonably expected to have recipients.
     * This is determined on whether the channel has default recipients, is subscribably, and
     * whether the send date time is far enough in the future to expect that if there are no
     * subscribers, there may actually be some by the time the notification is sent.
     * @param notification the notification to test
     * @return whether the specified Notification can be reasonably expected to have recipients
     */
    private boolean hasPotentialRecipients(NotificationBo notification) {
        LOG.info("notification channel " + notification.getChannel() + " is subscribable: "
                + notification.getChannel().isSubscribable());
        return notification.getChannel().getRecipientLists().size() > 0
                ||
                notification.getChannel().getSubscriptions().size() > 0
                ||
                (notification.getChannel().isSubscribable() && timeIsInTheFuture(notification.getSendDateTimeValue()
                        .getTime()));
    }

    protected NotificationService notificationService;

    protected NotificationWorkflowDocumentService notificationWorkflowDocService;

    protected NotificationChannelService notificationChannelService;

    protected NotificationRecipientService notificationRecipientService;

    protected NotificationMessageContentService messageContentService;

    protected GenericDao businessObjectDao;

    /**
     * Set the NotificationService
     * @param notificationService
     */
    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    /**
     * This method sets the NotificationWorkflowDocumentService
     * @param s
     */
    public void setNotificationWorkflowDocumentService(
            NotificationWorkflowDocumentService s) {
        this.notificationWorkflowDocService = s;
    }

    /**
     * Sets the notificationChannelService attribute value.
     * @param notificationChannelService The notificationChannelService to set.
     */
    public void setNotificationChannelService(
            NotificationChannelService notificationChannelService) {
        this.notificationChannelService = notificationChannelService;
    }

    /**
     * Sets the notificationRecipientService attribute value.
     * @param notificationRecipientService
     */
    public void setNotificationRecipientService(
            NotificationRecipientService notificationRecipientService) {
        this.notificationRecipientService = notificationRecipientService;
    }

    /**
     * Sets the messageContentService attribute value.
     * @param messageContentService
     */
    public void setMessageContentService(
            NotificationMessageContentService notificationMessageContentService) {
        this.messageContentService = notificationMessageContentService;
    }

    /**
     * Sets the businessObjectDao attribute value.
     * @param businessObjectDao The businessObjectDao to set.
     */
    public void setBusinessObjectDao(GenericDao businessObjectDao) {
        this.businessObjectDao = businessObjectDao;
    }

    /**
     * Handles the display of the form for sending a simple notification message
     * @param request : a servlet request
     * @param response : a servlet response
     * @throws ServletException : an exception
     * @throws IOException : an exception
     * @return a ModelAndView object
     */
    public ModelAndView sendSimpleNotificationMessage(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String view = "SendSimpleNotificationMessage";

        LOG.debug("remoteUser: " + request.getRemoteUser());

        Map<String, Object> model = setupModelForSendSimpleNotification(request);
        model.put("errors", new ErrorList()); // need an empty one so we don't have an NPE

        return new ModelAndView(view, model);
    }

    /**
     * This method prepares the model used for the send simple notification message form.
     * @param request
     * @return Map<String, Object>
     */
    private Map<String, Object> setupModelForSendSimpleNotification(
            HttpServletRequest request) {
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("defaultSender", request.getRemoteUser());
        model.put("channels", notificationChannelService
                .getAllNotificationChannels());
        model.put("priorities", businessObjectDao
                .findAll(NotificationPriorityBo.class));
        // set sendDateTime to current datetime if not provided
        String sendDateTime = request.getParameter("sendDateTime");
        String currentDateTime = Util.getCurrentDateTime();
        if (StringUtils.isEmpty(sendDateTime)) {
            sendDateTime = currentDateTime;
        }
        model.put("sendDateTime", sendDateTime);

        // retain the original date time or set to current if
        // it was not in the request
        if (request.getParameter("originalDateTime") == null) {
            model.put("originalDateTime", currentDateTime);
        } else {
            model.put("originalDateTime", request.getParameter("originalDateTime"));
        }

        model.put("userRecipients", request.getParameter("userRecipients"));
        model.put("workgroupRecipients", request.getParameter("workgroupRecipients"));
        model.put("workgroupNamespaceCodes", request.getParameter("workgroupNamespaceCodes"));

        return model;
    }

    /**
     * This method handles submitting the actual simple notification message.
     * @param request
     * @param response
     * @return ModelAndView
     * @throws ServletException
     * @throws IOException
     */
    public ModelAndView submitSimpleNotificationMessage(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        LOG.debug("remoteUser: " + request.getRemoteUser());

        // obtain a workflow user object first
        //WorkflowIdDTO initiator = new WorkflowIdDTO(request.getRemoteUser());
        String initiatorId = getPrincipalIdFromIdOrName( request.getRemoteUser());
        LOG.debug("initiatorId="+initiatorId);

        // now construct the workflow document, which will interact with workflow
        WorkflowDocument document;
        Map<String, Object> model = new HashMap<String, Object>();
        String view;
        try {
            document = NotificationWorkflowDocument.createNotificationDocument(
                    initiatorId,
                    NotificationConstants.KEW_CONSTANTS.SEND_NOTIFICATION_REQ_DOC_TYPE);

            //parse out the application content into a Notification BO
            NotificationBo notification = populateNotificationInstance(request,
                    model);

            // now get that content in an understandable XML format and pass into document
            String notificationAsXml = messageContentService
                    .generateNotificationMessage(notification);

            Map<String, String> attrFields = new HashMap<String, String>();
            List<NotificationChannelReviewerBo> reviewers = notification.getChannel().getReviewers();
            int ui = 0;
            int gi = 0;
            for (NotificationChannelReviewerBo reviewer : reviewers) {
                String prefix;
                int index;
                if (KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
                    prefix = "user";
                    index = ui;
                    ui++;
                } else if (KimGroupMemberTypes.GROUP_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
                    prefix = "group";
                    index = gi;
                    gi++;
                } else {
                    LOG.error("Invalid type for reviewer " + reviewer.getReviewerId() + ": "
                            + reviewer.getReviewerType());
                    continue;
                }
                attrFields.put(prefix + index, reviewer.getReviewerId());
            }
            GenericAttributeContent gac = new GenericAttributeContent("channelReviewers");
            document.setApplicationContent(notificationAsXml);
            document.setAttributeContent("<attributeContent>" + gac.generateContent(attrFields) + "</attributeContent>");

            document.setTitle(notification.getTitle());

            document.route("This message was submitted via the simple notification message submission form by user "
                    + initiatorId);

            view = "SendSimpleNotificationMessage";

            // This ain't pretty, but it gets the job done for now.
            ErrorList el = new ErrorList();
            el.addError("Notification(s) sent.");
            model.put("errors", el);

        } catch (ErrorList el) {
            // route back to the send form again
            Map<String, Object> model2 = setupModelForSendSimpleNotification(request);
            model.putAll(model2);
            model.put("errors", el);

            view = "SendSimpleNotificationMessage";
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return new ModelAndView(view, model);
    }

    /**
     * This method creates a new Notification instance from the form values.
     * @param request
     * @param model
     * @return Notification
     * @throws IllegalArgumentException
     */
    private NotificationBo populateNotificationInstance(
            HttpServletRequest request, Map<String, Object> model)
            throws IllegalArgumentException, ErrorList {
        ErrorList errors = new ErrorList();

        NotificationBo notification = new NotificationBo();

        // grab data from form
        // channel name
        String channelName = request.getParameter("channelName");
        if (StringUtils.isEmpty(channelName) || StringUtils.equals(channelName, NONE_CHANNEL)) {
            errors.addError("You must choose a channel.");
        } else {
            model.put("channelName", channelName);
        }

        // priority name
        String priorityName = request.getParameter("priorityName");
        if (StringUtils.isEmpty(priorityName)) {
            errors.addError("You must choose a priority.");
        } else {
            model.put("priorityName", priorityName);
        }

        // sender names
        String senderNames = request.getParameter("senderNames");
        String[] senders = null;
        if (StringUtils.isEmpty(senderNames)) {
            errors.addError("You must enter at least one sender.");
        } else {
            senders = StringUtils.split(senderNames, ",");

            model.put("senderNames", senderNames);
        }

        // delivery type
        String deliveryType = request.getParameter("deliveryType");
        if (StringUtils.isEmpty(deliveryType)) {
            errors.addError("You must choose a type.");
        } else {
            if (deliveryType
                    .equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
                deliveryType = NotificationConstants.DELIVERY_TYPES.FYI;
            } else {
                deliveryType = NotificationConstants.DELIVERY_TYPES.ACK;
            }
            model.put("deliveryType", deliveryType);
        }

        // get datetime when form was initially rendered
        String originalDateTime = request.getParameter("originalDateTime");
        Date origdate = null;
        Date senddate = null;
        Date removedate = null;
        try {
            origdate = Util.parseUIDateTime(originalDateTime);
        } catch (ParseException pe) {
            errors.addError("Original date is invalid.");
        }
        // send date time
        String sendDateTime = request.getParameter("sendDateTime");
        if (StringUtils.isBlank(sendDateTime)) {
            sendDateTime = Util.getCurrentDateTime();
        }

        try {
            senddate = Util.parseUIDateTime(sendDateTime);
        } catch (ParseException pe) {
            errors.addError("You specified an invalid Send Date/Time.  Please use the calendar picker.");
        }

        if (senddate != null && senddate.before(origdate)) {
            errors.addError("Send Date/Time cannot be in the past.");
        }

        model.put("sendDateTime", sendDateTime);

        // auto remove date time
        String autoRemoveDateTime = request.getParameter("autoRemoveDateTime");
        if (StringUtils.isNotBlank(autoRemoveDateTime)) {
            try {
                removedate = Util.parseUIDateTime(autoRemoveDateTime);
            } catch (ParseException pe) {
                errors.addError("You specified an invalid Auto-Remove Date/Time.  Please use the calendar picker.");
            }

            if (removedate != null) {
                if (removedate.before(origdate)) {
                    errors.addError("Auto-Remove Date/Time cannot be in the past.");
                } else if (senddate != null && removedate.before(senddate)) {
                    errors.addError("Auto-Remove Date/Time cannot be before the Send Date/Time.");
                }
            }
        }

        model.put("autoRemoveDateTime", autoRemoveDateTime);

        // user recipient names
        String[] userRecipients = parseUserRecipients(request);

        // workgroup recipient names
        String[] workgroupRecipients = parseWorkgroupRecipients(request);

        // workgroup namespace codes
        String[] workgroupNamespaceCodes = parseWorkgroupNamespaceCodes(request);

        // title
        String title = request.getParameter("title");
        if (!StringUtils.isEmpty(title)) {
            model.put("title", title);
        } else {
            errors.addError("You must fill in a title");
        }

        // message
        String message = request.getParameter("message");
        if (StringUtils.isEmpty(message)) {
            errors.addError("You must fill in a message.");
        } else {
            model.put("message", message);
        }

        // stop processing if there are errors
        if (errors.getErrors().size() > 0) {
            throw errors;
        }

        // now populate the notification BO instance
        NotificationChannelBo channel = Util.retrieveFieldReference("channel",
                "name", channelName, NotificationChannelBo.class,
                businessObjectDao);
        notification.setChannel(channel);

        NotificationPriorityBo priority = Util.retrieveFieldReference("priority",
                "name", priorityName, NotificationPriorityBo.class,
                businessObjectDao);
        notification.setPriority(priority);

        NotificationContentTypeBo contentType = Util.retrieveFieldReference(
                "contentType", "name",
                NotificationConstants.CONTENT_TYPES.SIMPLE_CONTENT_TYPE,
                NotificationContentTypeBo.class, businessObjectDao);
        notification.setContentType(contentType);

        NotificationProducerBo producer = Util
                .retrieveFieldReference(
                        "producer",
                        "name",
                        NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME,
                        NotificationProducerBo.class, businessObjectDao);
        notification.setProducer(producer);

        for (String senderName : senders) {
            if (StringUtils.isEmpty(senderName)) {
                errors.addError("A sender's name cannot be blank.");
            } else {
                NotificationSenderBo ns = new NotificationSenderBo();
                ns.setSenderName(senderName.trim());
                notification.addSender(ns);
            }
        }

        boolean recipientsExist = false;

        if (userRecipients != null && userRecipients.length > 0) {
            recipientsExist = true;
            for (String userRecipientId : userRecipients) {
                if (isUserRecipientValid(userRecipientId, errors)) {
                    NotificationRecipientBo recipient = new NotificationRecipientBo();
                    recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
                    recipient.setRecipientId(userRecipientId);
                    notification.addRecipient(recipient);
                }
            }
        }

        if (workgroupRecipients != null && workgroupRecipients.length > 0) {
            recipientsExist = true;
            if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
                if (workgroupNamespaceCodes.length == workgroupRecipients.length) {
                    for (int i = 0; i < workgroupRecipients.length; i++) {
                        if (isWorkgroupRecipientValid(workgroupRecipients[i], workgroupNamespaceCodes[i], errors)) {
                            NotificationRecipientBo recipient = new NotificationRecipientBo();
                            recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
                            recipient.setRecipientId(
                                    getGroupService().getGroupByNamespaceCodeAndName(workgroupNamespaceCodes[i],
                                            workgroupRecipients[i]).getId());
                            notification.addRecipient(recipient);
                        }
                    }
                } else {
                    errors.addError("The number of groups must match the number of namespace codes");
                }
            } else {
                errors.addError("You must specify a namespace code for every group name");
            }
        } else if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
            errors.addError("You must specify a group name for every namespace code");
        }

        // check to see if there were any errors
        if (errors.getErrors().size() > 0) {
            throw errors;
        }

        notification.setTitle(title);

        notification.setDeliveryType(deliveryType);

        // simpledateformat is not threadsafe, have to sync and validate
        Date d = null;
        if (StringUtils.isNotBlank(sendDateTime)) {
            try {
                d = Util.parseUIDateTime(sendDateTime);
            } catch (ParseException pe) {
                errors.addError("You specified an invalid send date and time.  Please use the calendar picker.");
            }
            notification.setSendDateTimeValue(new Timestamp(d.getTime()));
        }

        Date d2 = null;
        if (StringUtils.isNotBlank(autoRemoveDateTime)) {
            try {
                d2 = Util.parseUIDateTime(autoRemoveDateTime);
                if (d2.before(d)) {
                    errors.addError("Auto Remove Date/Time cannot be before Send Date/Time.");
                }
            } catch (ParseException pe) {
                errors.addError("You specified an invalid auto remove date and time.  Please use the calendar picker.");
            }
            notification.setAutoRemoveDateTimeValue(new Timestamp(d2.getTime()));
        }

        if (!recipientsExist && !hasPotentialRecipients(notification)) {
            errors.addError("You must specify at least one user or group recipient.");
        }

        // check to see if there were any errors
        if (errors.getErrors().size() > 0) {
            throw errors;
        }

        notification
                .setContent(NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_SIMPLE_OPEN
                        + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_OPEN
                        + message
                        + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_CLOSE
                        + NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_CLOSE);

        return notification;
    }
}
