/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *******************************************************************************/
package org.apache.ofbiz.webapp.event;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.UtilGenerics;
import org.apache.ofbiz.base.util.UtilHttp;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.transaction.GenericTransactionException;
import org.apache.ofbiz.entity.transaction.TransactionUtil;
import org.apache.ofbiz.service.DispatchContext;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.service.ModelParam;
import org.apache.ofbiz.service.ModelService;
import org.apache.ofbiz.service.ServiceAuthException;
import org.apache.ofbiz.service.ServiceUtil;
import org.apache.ofbiz.service.ServiceValidationException;
import org.apache.ofbiz.webapp.control.ConfigXMLReader;
import org.apache.ofbiz.webapp.control.ConfigXMLReader.Event;
import org.apache.ofbiz.webapp.control.ConfigXMLReader.RequestMap;
import org.apache.ofbiz.webapp.control.RequestHandler;
import org.apache.ofbiz.webapp.control.WebAppConfigurationException;

/**
 * ServiceMultiEventHandler - Event handler for running a service multiple times; for bulk forms
 */
public class ServiceMultiEventHandler implements EventHandler {

    public static final String module = ServiceMultiEventHandler.class.getName();

    public static final String SYNC = "sync";
    public static final String ASYNC = "async";

    protected ServletContext servletContext;

    /**
     * @see org.apache.ofbiz.webapp.event.EventHandler#init(javax.servlet.ServletContext)
     */
    public void init(ServletContext servletContext) throws EventHandlerException {
        this.servletContext = servletContext;
    }

    /**
     * @see org.apache.ofbiz.webapp.event.EventHandler#invoke(ConfigXMLReader.Event, ConfigXMLReader.RequestMap, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    public String invoke(Event event, RequestMap requestMap, HttpServletRequest request, HttpServletResponse response) throws EventHandlerException {
        // TODO: consider changing this to use the new UtilHttp.parseMultiFormData method

        // make sure we have a valid reference to the Service Engine
        LocalDispatcher dispatcher = (LocalDispatcher) request.getAttribute("dispatcher");
        if (dispatcher == null) {
            throw new EventHandlerException("The local service dispatcher is null");
        }
        DispatchContext dctx = dispatcher.getDispatchContext();
        if (dctx == null) {
            throw new EventHandlerException("Dispatch context cannot be found");
        }

        // get the details for the service(s) to call
        String mode = SYNC;
        String serviceName = null;

        if (UtilValidate.isEmpty(event.path)) {
            mode = SYNC;
        } else {
            mode = event.path;
        }

        // we only support SYNC mode in this handler
        if (!SYNC.equals(mode)) {
            throw new EventHandlerException("Async mode is not supported");
        }

        // nake sure we have a defined service to call
        serviceName = event.invoke;
        if (serviceName == null) {
            throw new EventHandlerException("Service name (eventMethod) cannot be null");
        }
        if (Debug.verboseOn()) Debug.logVerbose("[Set mode/service]: " + mode + "/" + serviceName, module);

        // some needed info for when running the service
        Locale locale = UtilHttp.getLocale(request);
        TimeZone timeZone = UtilHttp.getTimeZone(request);
        HttpSession session = request.getSession();
        GenericValue userLogin = (GenericValue) session.getAttribute("userLogin");

        // get the service model to generate context(s)
        ModelService modelService = null;

        try {
            modelService = dctx.getModelService(serviceName);
        } catch (GenericServiceException e) {
            throw new EventHandlerException("Problems getting the service model", e);
        }

        if (modelService == null) {
            throw new EventHandlerException("Problems getting the service model");
        }

        if (Debug.verboseOn()) Debug.logVerbose("[Processing]: SERVICE Event", module);
        if (Debug.verboseOn()) Debug.logVerbose("[Using delegator]: " + dispatcher.getDelegator().getDelegatorName(), module);

        // check if we are using per row submit
        boolean useRowSubmit = request.getParameter("_useRowSubmit") == null ? false :
                "Y".equalsIgnoreCase(request.getParameter("_useRowSubmit"));

        // check if we are to also look in a global scope (no delimiter)
        boolean checkGlobalScope = request.getParameter("_checkGlobalScope") == null ? true :
                !"N".equalsIgnoreCase(request.getParameter("_checkGlobalScope"));

        // The number of multi form rows is retrieved
        int rowCount = UtilHttp.getMultiFormRowCount(request);
        if (rowCount < 1) {
            throw new EventHandlerException("No rows to process");
        }

        // some default message settings
        String errorPrefixStr = UtilProperties.getMessage("DefaultMessagesUiLabels", "service.error.prefix", locale);
        String errorSuffixStr = UtilProperties.getMessage("DefaultMessagesUiLabels", "service.error.suffix", locale);
        String messagePrefixStr = UtilProperties.getMessage("DefaultMessagesUiLabels", "service.message.prefix", locale);
        String messageSuffixStr = UtilProperties.getMessage("DefaultMessagesUiLabels", "service.message.suffix", locale);

        // prepare the error message and success message lists
        List<Object> errorMessages = new LinkedList<Object>();
        List<String> successMessages = new LinkedList<String>();

        // Check the global-transaction attribute of the event from the controller to see if the
        //  event should be wrapped in a transaction
        String requestUri = RequestHandler.getRequestUri(request.getPathInfo());
        ConfigXMLReader.ControllerConfig controllerConfig;
        try {
            controllerConfig = ConfigXMLReader.getControllerConfig(ConfigXMLReader.getControllerConfigURL(servletContext));
        } catch (WebAppConfigurationException e) {
            throw new EventHandlerException(e);
        }
        boolean eventGlobalTransaction;
        try {
            eventGlobalTransaction = controllerConfig.getRequestMapMap().get(requestUri).event.globalTransaction;
        } catch (WebAppConfigurationException e) {
            throw new EventHandlerException(e);
        }

        Set<String> urlOnlyParameterNames = UtilHttp.getUrlOnlyParameterMap(request).keySet();

        // big try/finally to make sure commit or rollback are run
        boolean beganTrans = false;
        String returnString = null;
        try {
            if (eventGlobalTransaction) {
                // start the global transaction
                try {
                    beganTrans = TransactionUtil.begin(modelService.transactionTimeout * rowCount);
                } catch (GenericTransactionException e) {
                    throw new EventHandlerException("Problem starting multi-service global transaction", e);
                }
            }

            // now loop throw the rows and prepare/invoke the service for each
            for (int i = 0; i < rowCount; i++) {
                String curSuffix = UtilHttp.getMultiRowDelimiter() + i;
                boolean rowSelected = false;
                if (UtilValidate.isNotEmpty(request.getAttribute(UtilHttp.getRowSubmitPrefix() + i))) {
                    rowSelected = request.getAttribute(UtilHttp.getRowSubmitPrefix() + i) == null ? false :
                    "Y".equalsIgnoreCase((String)request.getAttribute(UtilHttp.getRowSubmitPrefix() + i));
                } else {
                    rowSelected = request.getParameter(UtilHttp.getRowSubmitPrefix() + i) == null ? false :
                    "Y".equalsIgnoreCase(request.getParameter(UtilHttp.getRowSubmitPrefix() + i));
                }

                // make sure we are to process this row
                if (useRowSubmit && !rowSelected) {
                    continue;
                }

                // build the context
                Map<String, Object> serviceContext = new HashMap<String, Object>();
                for (ModelParam modelParam: modelService.getInModelParamList()) {
                    String paramName = modelParam.name;

                    // Debug.logInfo("In ServiceMultiEventHandler processing input parameter [" + modelParam.name + (modelParam.optional?"(optional):":"(required):") + modelParam.mode + "] for service [" + serviceName + "]", module);

                    // don't include userLogin, that's taken care of below
                    if ("userLogin".equals(paramName)) continue;
                    // don't include locale, that is also taken care of below
                    if ("locale".equals(paramName)) continue;
                    // don't include timeZone, that is also taken care of below
                    if ("timeZone".equals(paramName)) continue;

                    Object value = null;
                    if (UtilValidate.isNotEmpty(modelParam.stringMapPrefix)) {
                        Map<String, Object> paramMap = UtilHttp.makeParamMapWithPrefix(request, modelParam.stringMapPrefix, curSuffix);
                        value = paramMap;
                    } else if (UtilValidate.isNotEmpty(modelParam.stringListSuffix)) {
                        List<Object> paramList = UtilHttp.makeParamListWithSuffix(request, modelParam.stringListSuffix, null);
                        value = paramList;
                    } else {
                        // check attributes; do this before parameters so that attribute which can be changed by code can override parameters which can't
                        value = request.getAttribute(paramName + curSuffix);

                        // first check for request parameters
                        if (value == null) {
                            String name = paramName + curSuffix;

                            ServiceEventHandler.checkSecureParameter(requestMap, urlOnlyParameterNames, name, session, serviceName, dctx.getDelegator());

                            String[] paramArr = request.getParameterValues(name);
                            if (paramArr != null) {
                                if (paramArr.length > 1) {
                                    value = Arrays.asList(paramArr);
                                } else {
                                    value = paramArr[0];
                                }
                            }
                        }

                        // if the parameter wasn't passed and no other value found, check the session
                        if (value == null) {
                            value = session.getAttribute(paramName + curSuffix);
                        }

                        // now check global scope
                        if (value == null) {
                            if (checkGlobalScope) {
                                String[] gParamArr = request.getParameterValues(paramName);
                                if (gParamArr != null) {
                                    if (gParamArr.length > 1) {
                                        value = Arrays.asList(gParamArr);
                                    } else {
                                        value = gParamArr[0];
                                    }
                                }
                                if (value == null) {
                                    value = request.getAttribute(paramName);
                                }
                                if (value == null) {
                                    value = session.getAttribute(paramName);
                                }
                            }
                        }

                        // make any composite parameter data (e.g., from a set of parameters {name_c_date, name_c_hour, name_c_minutes})
                        if (value == null) {
                            value = UtilHttp.makeParamValueFromComposite(request, paramName + curSuffix, locale);
                        }

                        if (value == null) {
                            // still null, give up for this one
                            continue;
                        }

                        if (value instanceof String && ((String) value).length() == 0) {
                            // interpreting empty fields as null values for each in back end handling...
                            value = null;
                        }
                    }
                    // set even if null so that values will get nulled in the db later on
                    serviceContext.put(paramName, value);

                    // Debug.logInfo("In ServiceMultiEventHandler got value [" + value + "] for input parameter [" + paramName + "] for service [" + serviceName + "]", module);
                }

                // get only the parameters for this service - converted to proper type
                serviceContext = modelService.makeValid(serviceContext, ModelService.IN_PARAM, true, null, timeZone, locale);

                // include the UserLogin value object
                if (userLogin != null) {
                    serviceContext.put("userLogin", userLogin);
                }

                // include the Locale object
                if (locale != null) {
                    serviceContext.put("locale", locale);
                }

                // include the TimeZone object
                if (timeZone != null) {
                    serviceContext.put("timeZone", timeZone);
                }

                // Debug.logInfo("ready to call " + serviceName + " with context " + serviceContext, module);

                // invoke the service
                Map<String, Object> result = null;
                try {
                    result = dispatcher.runSync(serviceName, serviceContext);
                } catch (ServiceAuthException e) {
                    // not logging since the service engine already did
                    errorMessages.add(messagePrefixStr + "Service invocation error on row (" + i +"): " + e.getNonNestedMessage());
                } catch (ServiceValidationException e) {
                    // not logging since the service engine already did
                    request.setAttribute("serviceValidationException", e);
                    List<String> errors = e.getMessageList();
                    if (errors != null) {
                        for (String message: errors) {
                            errorMessages.add("Service invocation error on row (" + i + "): " + message);
                        }
                    } else {
                        errorMessages.add(messagePrefixStr + "Service invocation error on row (" + i +"): " + e.getNonNestedMessage());
                    }
                } catch (GenericServiceException e) {
                    Debug.logError(e, "Service invocation error", module);
                    errorMessages.add(messagePrefixStr + "Service invocation error on row (" + i +"): " + e.getNested() + messageSuffixStr);
                }

                if (result == null) {
                    returnString = ModelService.RESPOND_SUCCESS;
                } else {
                    // check for an error message
                    String errorMessage = ServiceUtil.makeErrorMessage(result, messagePrefixStr, messageSuffixStr, "", "");
                    if (UtilValidate.isNotEmpty(errorMessage)) {
                        errorMessages.add(errorMessage);
                    }

                    // get the success messages
                    if (UtilValidate.isNotEmpty(result.get(ModelService.SUCCESS_MESSAGE))) {
                        String newSuccessMessage = (String)result.get(ModelService.SUCCESS_MESSAGE);
                        if (!successMessages.contains(newSuccessMessage)) {
                            successMessages.add(newSuccessMessage);
                        }
                    }
                    if (UtilValidate.isNotEmpty(result.get(ModelService.SUCCESS_MESSAGE_LIST))) {
                        List<String> newSuccessMessages = UtilGenerics.<String>checkList(result.get(ModelService.SUCCESS_MESSAGE_LIST));
                        for (int j = 0; j < newSuccessMessages.size(); j++) {
                            String newSuccessMessage = newSuccessMessages.get(j);
                            if (!successMessages.contains(newSuccessMessage)) {
                                successMessages.add(newSuccessMessage);
                            }
                        }
                    }
                }
                // set the results in the request
                if ((result != null) && (result.entrySet() != null)) {
                    for (Map.Entry<String, Object> rme: result.entrySet()) {
                        String resultKey = rme.getKey();
                        Object resultValue = rme.getValue();

                        if (resultKey != null && !ModelService.RESPONSE_MESSAGE.equals(resultKey) && !ModelService.ERROR_MESSAGE.equals(resultKey) &&
                                !ModelService.ERROR_MESSAGE_LIST.equals(resultKey) && !ModelService.ERROR_MESSAGE_MAP.equals(resultKey) &&
                                !ModelService.SUCCESS_MESSAGE.equals(resultKey) && !ModelService.SUCCESS_MESSAGE_LIST.equals(resultKey)) {
                            //set the result to request w/ and w/o a suffix to handle both cases: to have the result in each iteration and to prevent its overriding
                            request.setAttribute(resultKey + curSuffix, resultValue);
                            request.setAttribute(resultKey, resultValue);
                        }
                    }
                }
            }
        } finally {
            if (errorMessages.size() > 0) {
                if (eventGlobalTransaction) {
                    // rollback the global transaction
                    try {
                        TransactionUtil.rollback(beganTrans, "Error in multi-service event handling: " + errorMessages.toString(), null);
                    } catch (GenericTransactionException e) {
                        Debug.logError(e, "Could not rollback multi-service global transaction", module);
                    }
                }
                errorMessages.add(0, errorPrefixStr);
                errorMessages.add(errorSuffixStr);
                StringBuilder errorBuf = new StringBuilder();
                for (Object em: errorMessages) {
                    errorBuf.append(em + "\n");
                }
                request.setAttribute("_ERROR_MESSAGE_", errorBuf.toString());
                returnString = "error";
            } else {
                if (eventGlobalTransaction) {
                    // commit the global transaction
                    try {
                        TransactionUtil.commit(beganTrans);
                    } catch (GenericTransactionException e) {
                        Debug.logError(e, "Could not commit multi-service global transaction", module);
                        throw new EventHandlerException("Commit multi-service global transaction failed");
                    }
                }
                if (successMessages.size() > 0) {
                    request.setAttribute("_EVENT_MESSAGE_LIST_", successMessages);
                }
                returnString = "success";
            }
        }

        return returnString;
    }
}
