/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.module.cab.document.web.struts;


import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.kns.question.ConfirmationQuestion;
import org.kuali.kfs.kns.util.KNSGlobalVariables;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.krad.util.KRADUtils;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.cab.CabConstants;
import org.kuali.kfs.module.cab.CabKeyConstants;
import org.kuali.kfs.module.cab.CabPropertyConstants;
import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableDocument;
import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset;
import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableLineAssetAccount;
import org.kuali.kfs.module.cab.document.service.PurApInfoService;
import org.kuali.kfs.module.cab.document.service.PurApLineDocumentService;
import org.kuali.kfs.module.cab.document.service.PurApLineService;
import org.kuali.kfs.module.cab.document.web.PurApLineSession;
import org.kuali.kfs.module.cam.CamsConstants;
import org.kuali.kfs.module.cam.CamsKeyConstants;
import org.kuali.kfs.module.cam.businessobject.AssetGlobal;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.util.RiceKeyConstants;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.exception.WorkflowException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PurApLineAction extends CabActionBase {
    private static final Logger LOG = Logger.getLogger(PurApLineAction.class);
    PurApLineService purApLineService = SpringContext.getBean(PurApLineService.class);
    PurApInfoService purApInfoService = SpringContext.getBean(PurApInfoService.class);
    PurApLineDocumentService purApLineDocumentService = SpringContext.getBean(PurApLineDocumentService.class);

    /**
     * Handle start action.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward start(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApLineForm = (PurApLineForm) form;
        if (purApLineForm.getPurchaseOrderIdentifier() == null) {
            GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, CabKeyConstants.ERROR_PO_ID_EMPTY);
        } else {
            // set Contact Email Address and Phone Number from PurAp Purchase Order document
            purApInfoService.setPurchaseOrderFromPurAp(purApLineForm);

            // save PurAp document list into form
            buildPurApDocList(purApLineForm);

            if (!purApLineForm.getPurApDocs().isEmpty()) {
                // set item pre-populated fields
                purApLineService.buildPurApItemAssetList(purApLineForm.getPurApDocs());
                // create session object for current processing
                createPurApLineSession(purApLineForm.getPurchaseOrderIdentifier());
            }
        }
        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    protected void createPurApLineSession(Integer purchaseOrderIdentifier) {
        GlobalVariables.getUserSession().addObject(CabConstants.CAB_PURAP_SESSION.concat(purchaseOrderIdentifier.toString()), new PurApLineSession());
    }

    protected void clearPurApLineSession(Integer purchaseOrderIdentifier) {
        if (purchaseOrderIdentifier != null) {
            GlobalVariables.getUserSession().removeObject(CabConstants.CAB_PURAP_SESSION.concat(purchaseOrderIdentifier.toString()));
        }
    }

    public ActionForward reload(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApLineForm = (PurApLineForm) form;
        if (purApLineForm.getPurchaseOrderIdentifier() == null) {
            GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, CabKeyConstants.ERROR_PO_ID_EMPTY);
        }
        purApLineForm.getPurApDocs().clear();
        // clear the session and reload the page
        clearPurApLineSession(purApLineForm.getPurchaseOrderIdentifier());
        return start(mapping, form, request, response);
    }

    /**
     * Build PurchasingAccountsPayableDocument list in which all documents have the same PO_ID.
     *
     * @param purApLineForm
     */
    protected void buildPurApDocList(PurApLineForm purApLineForm) {
        Map<String, Object> cols = new HashMap<String, Object>();
        cols.put(CabPropertyConstants.PurchasingAccountsPayableDocument.PURCHASE_ORDER_IDENTIFIER, purApLineForm.getPurchaseOrderIdentifier());
        Collection<PurchasingAccountsPayableDocument> purApDocs = SpringContext.getBean(BusinessObjectService.class).findMatchingOrderBy(PurchasingAccountsPayableDocument.class, cols, CabPropertyConstants.PurchasingAccountsPayableDocument.DOCUMENT_NUMBER, true);

        if (purApDocs == null || purApDocs.isEmpty()) {
            GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, CabKeyConstants.ERROR_PO_ID_INVALID, purApLineForm.getPurchaseOrderIdentifier().toString());
        } else {
            boolean existActiveDoc = false;
            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
                if (ObjectUtils.isNotNull(purApDoc) && purApDoc.isActive()) {
                    // If there exists active document, set the existActiveDoc indicator.
                    existActiveDoc = true;
                    break;
                }
            }
            purApLineForm.getPurApDocs().clear();
            purApLineForm.getPurApDocs().addAll(purApDocs);
            setupObjectRelationship(purApLineForm.getPurApDocs());
            // If no active item exists or no exist document, display a message.
            if (!existActiveDoc) {
                KNSGlobalVariables.getMessageList().add(CabKeyConstants.MESSAGE_NO_ACTIVE_PURAP_DOC);
            }
        }
    }

    /**
     * Setup relationship from account to item and item to doc. In this way, we keep all working objects in the same view as form.
     *
     * @param purApDocs
     */
    protected void setupObjectRelationship(List<PurchasingAccountsPayableDocument> purApDocs) {
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                item.setPurchasingAccountsPayableDocument(purApDoc);
                for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
                    account.setPurchasingAccountsPayableItemAsset(item);
                }
            }
        }
    }

    /**
     * Cancels the action and returns to portal main page
     *
     * @param mapping  {@link ActionMapping}
     * @param form     {@link ActionForm}
     * @param request  {@link HttpServletRequest}
     * @param response {@link HttpServletResponse}
     * @return {@link ActionForward}
     * @throws Exception
     */
    public ActionForward cancel(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        return mapping.findForward(KRADConstants.MAPPING_PORTAL);
    }

    /**
     * save the information in the current form into underlying data store
     */
    public ActionForward save(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApLineForm = (PurApLineForm) form;
        // get the current processing object from session
        PurApLineSession purApLineSession = retrievePurApLineSession(purApLineForm);
        // persistent changes to CAB tables
        purApLineService.processSaveBusinessObjects(purApLineForm.getPurApDocs(), purApLineSession);
        KNSGlobalVariables.getMessageList().add(CabKeyConstants.MESSAGE_CAB_CHANGES_SAVED_SUCCESS);
        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * Handling for screen close. Default action is return to caller.
     */
    public ActionForward close(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApLineForm = (PurApLineForm) form;

        // Create question page for save before close.
        Object question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
        ConfigurationService kualiConfiguration = SpringContext.getBean(ConfigurationService.class);

        // logic for close question
        if (question == null) {
            // ask question if not already asked
            return this.performQuestionWithoutInput(mapping, form, request, response, KRADConstants.DOCUMENT_SAVE_BEFORE_CLOSE_QUESTION, kualiConfiguration.getPropertyValueAsString(RiceKeyConstants.QUESTION_SAVE_BEFORE_CLOSE), KRADConstants.CONFIRMATION_QUESTION, KRADConstants.MAPPING_CLOSE, "");
        } else {
            Object buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
            PurApLineSession purApLineSession = retrievePurApLineSession(purApLineForm);
            if ((KRADConstants.DOCUMENT_SAVE_BEFORE_CLOSE_QUESTION.equals(question)) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                purApLineService.processSaveBusinessObjects(purApLineForm.getPurApDocs(), purApLineSession);
            }
            // remove current processing object from session
            removePurApLineSession(purApLineForm.getPurchaseOrderIdentifier());
        }

        return mapping.findForward(KRADConstants.MAPPING_PORTAL);
    }

    /**
     * Remove PurApLineSession object from user session.
     *
     * @param purApLineForm
     */
    private void removePurApLineSession(Integer purchaseOrderIdentifier) {
        GlobalVariables.getUserSession().removeObject(CabConstants.CAB_PURAP_SESSION.concat(purchaseOrderIdentifier.toString()));
    }


    /**
     * This method handles split action. Create one item with split quantity
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward split(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApLineForm = (PurApLineForm) form;
        // Get the line item for applying split action.
        PurchasingAccountsPayableItemAsset selectedLineItem = getSelectedLineItem((PurApLineForm) form);

        if (selectedLineItem == null) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        String errorPath = CabPropertyConstants.PurApLineForm.PURAP_DOCS + KFSConstants.SQUARE_BRACKET_LEFT + purApLineForm.getActionPurApDocIndex() + KFSConstants.SQUARE_BRACKET_RIGHT + "." + CabPropertyConstants.PurchasingAccountsPayableDocument.PURCHASEING_ACCOUNTS_PAYABLE_ITEM_ASSETS + KFSConstants.SQUARE_BRACKET_LEFT + purApLineForm.getActionItemAssetIndex() + KFSConstants.SQUARE_BRACKET_RIGHT;
        GlobalVariables.getMessageMap().addToErrorPath(errorPath);
        // check user input split quantity.
        checkSplitQty(selectedLineItem, errorPath);
        GlobalVariables.getMessageMap().removeFromErrorPath(errorPath);

        // apply split when error free
        if (GlobalVariables.getMessageMap().hasNoErrors() && selectedLineItem != null) {
            PurApLineSession purApLineSession = retrievePurApLineSession(purApLineForm);
            // create a new item with split quantity from selected item
            purApLineService.processSplit(selectedLineItem, purApLineSession.getActionsTakenHistory());
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * Get PurApLineSession object from user session.
     *
     * @param purApLineForm
     * @return
     */
    private PurApLineSession retrievePurApLineSession(PurApLineForm purApForm) {
        PurApLineSession purApLineSession = (PurApLineSession) GlobalVariables.getUserSession().retrieveObject(CabConstants.CAB_PURAP_SESSION.concat(purApForm.getPurchaseOrderIdentifier().toString()));
        if (purApLineSession == null) {
            purApLineSession = new PurApLineSession();
            GlobalVariables.getUserSession().addObject(CabConstants.CAB_PURAP_SESSION.concat(purApForm.getPurchaseOrderIdentifier().toString()), purApLineSession);
        }
        return purApLineSession;
    }

    /**
     * Check user input splitQty. It must be required and can't be zero or greater than current quantity.
     *
     * @param itemAsset
     * @param errorPath
     */
    protected void checkSplitQty(PurchasingAccountsPayableItemAsset itemAsset, String errorPath) {
        if (itemAsset.getSplitQty() == null)
            itemAsset.setSplitQty(KualiDecimal.ZERO);

        if (itemAsset.getAccountsPayableItemQuantity() == null)
            itemAsset.setAccountsPayableItemQuantity(KualiDecimal.ZERO);

        KualiDecimal splitQty = itemAsset.getSplitQty();
        KualiDecimal oldQty = itemAsset.getAccountsPayableItemQuantity();
        KualiDecimal maxAllowQty = oldQty.subtract(new KualiDecimal(0.1));

        if (splitQty == null) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurchasingAccountsPayableItemAsset.SPLIT_QTY, CabKeyConstants.ERROR_SPLIT_QTY_REQUIRED);
        } else if (splitQty.isLessEqual(KualiDecimal.ZERO) || splitQty.isGreaterEqual(oldQty)) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurchasingAccountsPayableItemAsset.SPLIT_QTY, CabKeyConstants.ERROR_SPLIT_QTY_INVALID, maxAllowQty.toString());
        }
        return;
    }

    /**
     * Merge Action includes merge all functionality.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward merge(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApForm = (PurApLineForm) form;
        boolean isMergeAll = purApLineService.isMergeAllAction(purApForm.getPurApDocs());
        List<PurchasingAccountsPayableItemAsset> mergeLines = purApLineService.getSelectedMergeLines(isMergeAll, purApForm.getPurApDocs());

        Object question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
        // logic for trade-in allowance question
        if (question != null) {
            Object buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
            if (CabConstants.TRADE_IN_INDICATOR_QUESTION.equals(question) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                if (purApLineService.mergeLinesHasDifferentObjectSubTypes(mergeLines)) {
                    // check if objectSubTypes are different and bring the obj sub types warning message
                    String warningMessage = generateObjectSubTypeQuestion();
                    return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.PAYMENT_DIFFERENT_OBJECT_SUB_TYPE_CONFIRMATION_QUESTION, warningMessage, KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.MERGE, "");
                } else {
                    performMerge(purApForm, mergeLines, isMergeAll);
                }
            } else if (CabConstants.PAYMENT_DIFFERENT_OBJECT_SUB_TYPE_CONFIRMATION_QUESTION.equals(question) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                performMerge(purApForm, mergeLines, isMergeAll);
            }

            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        boolean tradeInAllowanceInAllLines = purApLineService.isTradeInAllowanceExist(purApForm.getPurApDocs());
        boolean tradeInIndicatorInSelectedLines = purApLineService.isTradeInIndExistInSelectedLines(mergeLines);
        // validating...
        validateMergeAction(purApForm, mergeLines, isMergeAll, tradeInAllowanceInAllLines, tradeInIndicatorInSelectedLines);

        if (GlobalVariables.getMessageMap().hasNoErrors()) {
            // Display a warning message without blocking the action if TI indicator exists but TI allowance not exist.
            if (tradeInIndicatorInSelectedLines && !tradeInAllowanceInAllLines) {
                return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.TRADE_IN_INDICATOR_QUESTION, SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(CabKeyConstants.QUESTION_TRADE_IN_INDICATOR_EXISTING), KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.MERGE, "");
            } else if (purApLineService.mergeLinesHasDifferentObjectSubTypes(mergeLines)) {
                // check if objectSubTypes are different and bring the obj sub types warning message
                String warningMessage = generateObjectSubTypeQuestion();
                return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.PAYMENT_DIFFERENT_OBJECT_SUB_TYPE_CONFIRMATION_QUESTION, warningMessage, KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.MERGE, "");
            } else {
                performMerge(purApForm, mergeLines, isMergeAll);
            }
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }


    /**
     * Generate the question string for different object sub type codes.
     *
     * @return
     */
    protected String generateObjectSubTypeQuestion() {
        String parameterDetail = "(module:" + KRADUtils.getNamespaceAndComponentSimpleName(AssetGlobal.class) + ")";
        ConfigurationService kualiConfiguration = this.getConfigurationService();

        String continueQuestion = kualiConfiguration.getPropertyValueAsString(CamsKeyConstants.CONTINUE_QUESTION);
        String warningMessage = kualiConfiguration.getPropertyValueAsString(CabKeyConstants.QUESTION_DIFFERENT_OBJECT_SUB_TYPES) + " " + CamsConstants.Parameters.OBJECT_SUB_TYPE_GROUPS + " " + parameterDetail + ". " + continueQuestion;
        return warningMessage;
    }

    /**
     * Merge with service help.
     *
     * @param purApForm
     * @param mergeLines
     */
    protected void performMerge(PurApLineForm purApForm, List<PurchasingAccountsPayableItemAsset> mergeLines, boolean isMergeAll) {
        PurApLineSession purApLineSession = retrievePurApLineSession(purApForm);
        // handle merging lines including merge all situation.
        retrieveUserInputForMerge(mergeLines.get(0), purApForm);
        purApLineService.processMerge(mergeLines, purApLineSession.getActionsTakenHistory(), isMergeAll);
        // add all other mergeLines except the first one into processedItem list.
        mergeLines.remove(0);
        purApLineSession.getProcessedItems().addAll(mergeLines);
        clearForMerge(purApForm);
    }

    /**
     * Retrieve user input merge quantity and merge description.
     *
     * @param firstItem
     * @param purApForm
     */
    protected void retrieveUserInputForMerge(PurchasingAccountsPayableItemAsset firstItem, PurApLineForm purApForm) {
        if (ObjectUtils.isNotNull(firstItem)) {
            // Set new value for quantity and description.
            firstItem.setAccountsPayableItemQuantity(purApForm.getMergeQty());
            firstItem.setAccountsPayableLineItemDescription(purApForm.getMergeDesc());
        }
    }

    /**
     * Clear user input after merge.
     *
     * @param purApForm
     */
    protected void clearForMerge(PurApLineForm purApForm) {
        // reset user input values.
        purApLineService.resetSelectedValue(purApForm.getPurApDocs());
        purApForm.setMergeQty(null);
        purApForm.setMergeDesc(null);
        purApForm.setSelectAll(false);
    }

    /**
     * Check if the merge action is valid or not.
     *
     * @param purApForm
     * @param mergeLines
     */
    protected void validateMergeAction(PurApLineForm purApForm, List<PurchasingAccountsPayableItemAsset> mergeLines, boolean isMergeAll, boolean tradeInAllowanceInAllLines, boolean tradeInIndicatorInSelectedLines) {
        // check if the user entered merge quantity and merge description
        checkMergeRequiredFields(purApForm);

        // Check if the selected merge lines violate the business constraints.
        if (isMergeAll) {
            checkMergeAllValid(tradeInAllowanceInAllLines, tradeInIndicatorInSelectedLines);
        } else {
            checkMergeLinesValid(mergeLines, tradeInAllowanceInAllLines, tradeInIndicatorInSelectedLines);
        }

        // Check the associated pre-tagging data entries.
        checkPreTagValidForMerge(mergeLines, purApForm.getPurchaseOrderIdentifier());
    }

    /**
     * If to be merged items have: (1) No Pretag data: No problem; (2) 1 Pretag data entry: Associate this one with the new item
     * created after merge;(3) 1+ Pretag data entries: Display error, user has to manually fix data
     *
     * @param mergeLines
     */
    protected void checkPreTagValidForMerge(List<PurchasingAccountsPayableItemAsset> mergeLines, Integer purchaseOrderIdentifier) {
        Map validNumberMap = getItemLineNumberMap(mergeLines);

        if (!validNumberMap.isEmpty() && validNumberMap.size() > 1 && purApLineService.isMultipleTagExisting(purchaseOrderIdentifier, validNumberMap.keySet())) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_MERGE_WITH_PRETAGGING);
        }
    }

    /**
     * Build a Hashmap for itemLineNumber since itemLines could exist duplicate itemLineNumber
     *
     * @param itemLines
     * @return
     */
    protected Map getItemLineNumberMap(List<PurchasingAccountsPayableItemAsset> itemLines) {
        Map validNumberMap = new HashMap<Integer, Integer>();
        for (PurchasingAccountsPayableItemAsset item : itemLines) {
            if (item.getItemLineNumber() != null) {
                validNumberMap.put(item.getItemLineNumber(), item.getItemLineNumber());
            }
        }
        return validNumberMap;
    }

    /**
     * For merge all, check if exists Trade-in allowance pending for allocate.
     *
     * @param mergeLines
     * @param purApForm
     */
    protected void checkMergeAllValid(boolean tradeInAllowanceInAllLines, boolean tradeInIndicatorInSelectedLines) {
        if (tradeInAllowanceInAllLines && tradeInIndicatorInSelectedLines) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_TRADE_IN_PENDING);
        }
    }

    /**
     * Check if merge lines selected are allowed to continue this action. Constraints include: merge lines must more than 1; no
     * additional charges pending for allocate.
     *
     * @param mergeLines
     * @param purApForm
     */
    protected void checkMergeLinesValid(List<PurchasingAccountsPayableItemAsset> mergeLines, boolean tradeInAllowanceInAllLines, boolean tradeInIndicatorInSelectedLines) {
        if (mergeLines.size() <= 1) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_MERGE_LINE_SELECTED);
        } else {
            // if merge for different document lines and that document has additional charge allocation pending, additional charges
            // should be allocated first.
            if (purApLineService.isAdditionalChargePending(mergeLines)) {
                GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_ADDL_CHARGE_PENDING);
            }

            // if merge lines has indicator exists and trade-in allowance is pending for allocation, we will block this action.
            if (tradeInIndicatorInSelectedLines && tradeInAllowanceInAllLines) {
                GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_TRADE_IN_PENDING);
            }
        }
    }


    /**
     * Check the required fields entered for merge.
     */
    protected void checkMergeRequiredFields(PurApLineForm purApForm) {
        if (purApForm.getMergeQty() == null) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.MERGE_QTY, CabKeyConstants.ERROR_MERGE_QTY_EMPTY);
        }

        if (StringUtils.isBlank(purApForm.getMergeDesc())) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.MERGE_DESC, CabKeyConstants.ERROR_MERGE_DESCRIPTION_EMPTY);
        }
    }


    /**
     * Update the item quantity value from a decimal(less than 1) to 1.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward percentPayment(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApform = (PurApLineForm) form;
        PurchasingAccountsPayableItemAsset itemAsset = getSelectedLineItem(purApform);

        if (itemAsset != null) {
            PurApLineSession purApLineSession = retrievePurApLineSession(purApform);
            purApLineService.processPercentPayment(itemAsset, purApLineSession.getActionsTakenHistory());
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * Allocate line items including allocate additional charges functionality.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward allocate(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApForm = (PurApLineForm) form;
        PurchasingAccountsPayableItemAsset allocateSourceLine = getSelectedLineItem(purApForm);
        if (allocateSourceLine == null) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }
        List<PurchasingAccountsPayableItemAsset> allocateTargetLines = purApLineService.getAllocateTargetLines(allocateSourceLine, purApForm.getPurApDocs());

        Object question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
        // logic for trade-in allowance question
        if (question != null) {
            Object buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
            if ((CabConstants.TRADE_IN_INDICATOR_QUESTION.equals(question)) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                if (purApLineService.allocateLinesHasDifferentObjectSubTypes(allocateTargetLines, allocateSourceLine)) {
                    // check if objectSubTypes are different and bring the obj sub types warning message
                    String warningMessage = generateObjectSubTypeQuestion();
                    return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.PAYMENT_DIFFERENT_OBJECT_SUB_TYPE_CONFIRMATION_QUESTION, warningMessage, KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.ALLOCATE, "");
                } else {
                    performAllocate(purApForm, allocateSourceLine, allocateTargetLines);
                }
            } else if (CabConstants.PAYMENT_DIFFERENT_OBJECT_SUB_TYPE_CONFIRMATION_QUESTION.equals(question) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                performAllocate(purApForm, allocateSourceLine, allocateTargetLines);
            }

            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        boolean targetLineHasTradeIn = purApLineService.isTradeInIndExistInSelectedLines(allocateTargetLines);
        boolean hasTradeInAllowance = purApLineService.isTradeInAllowanceExist(purApForm.getPurApDocs());
        // Check if this allocate is valid.
        validateAllocateAction(allocateSourceLine, allocateTargetLines, targetLineHasTradeIn, hasTradeInAllowance, purApForm.getPurApDocs());
        if (GlobalVariables.getMessageMap().hasNoErrors()) {
            // TI indicator exists in either source or target lines, but TI allowance not found, bring up a warning message
            // to confirm this action.
            if (!allocateSourceLine.isAdditionalChargeNonTradeInIndicator() && !allocateSourceLine.isTradeInAllowance() && (allocateSourceLine.isItemAssignedToTradeInIndicator() || targetLineHasTradeIn) && hasTradeInAllowance) {
                return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.TRADE_IN_INDICATOR_QUESTION, SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(CabKeyConstants.QUESTION_TRADE_IN_INDICATOR_EXISTING), KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.ALLOCATE, "");
            } else if (purApLineService.allocateLinesHasDifferentObjectSubTypes(allocateTargetLines, allocateSourceLine)) {
                // check if objectSubTypes are different and bring the obj sub types warning message
                String warningMessage = generateObjectSubTypeQuestion();
                return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.PAYMENT_DIFFERENT_OBJECT_SUB_TYPE_CONFIRMATION_QUESTION, warningMessage, KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.ALLOCATE, "");
            } else {
                performAllocate(purApForm, allocateSourceLine, allocateTargetLines);
            }
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * Allocate with service help.
     *
     * @param purApForm
     * @param allocateSourceLine
     * @param allocateTargetLines
     */
    protected void performAllocate(PurApLineForm purApForm, PurchasingAccountsPayableItemAsset allocateSourceLine, List<PurchasingAccountsPayableItemAsset> allocateTargetLines) {
        PurApLineSession purApLineSession = retrievePurApLineSession(purApForm);
        if (!purApLineService.processAllocate(allocateSourceLine, allocateTargetLines, purApLineSession.getActionsTakenHistory(), purApForm.getPurApDocs(), false)) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_ALLOCATE_NO_TARGET_ACCOUNT);
        } else {
            purApLineSession.getProcessedItems().add(allocateSourceLine);
            // clear select check box
            purApLineService.resetSelectedValue(purApForm.getPurApDocs());
            purApForm.setSelectAll(false);
        }
    }

    /**
     * Check if the line items are allowed to allocate.
     *
     * @param selectedLine
     * @param allocateTargetLines
     * @param purApForm
     */
    protected void validateAllocateAction(PurchasingAccountsPayableItemAsset allocateSourceLine, List<PurchasingAccountsPayableItemAsset> allocateTargetLines, boolean targetLineHasTradeIn, boolean hasTradeInAllowance, List<PurchasingAccountsPayableDocument> purApDocs) {
        // if no target selected...
        if (allocateTargetLines.isEmpty()) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_ALLOCATE_NO_LINE_SELECTED);
        }

        // For allocate trade-in allowance, additional charges(non trade-in) must be allocated before allocate trade-in.
        if (allocateSourceLine.isTradeInAllowance() && purApLineService.isAdditionalChargeExistInAllLines(purApDocs)) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_ADDL_CHARGE_PENDING);
        }
        // For line item, we need to check...
        if (!allocateSourceLine.isAdditionalChargeNonTradeInIndicator() && !allocateSourceLine.isTradeInAllowance()) {
            allocateTargetLines.add(allocateSourceLine);
            // Pending additional charges(non trade-in) can't associate with either source line or target lines.
            if (purApLineService.isAdditionalChargePending(allocateTargetLines)) {
                GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_ADDL_CHARGE_PENDING);
            }

            // For line item, check if trade-in allowance allocation pending.
            if (targetLineHasTradeIn && hasTradeInAllowance) {
                GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_TRADE_IN_PENDING);
            }
            allocateTargetLines.remove(allocateSourceLine);
        }

    }

    /**
     * Get the user selected line item and set the link reference to document
     *
     * @param purApLineForm
     * @return
     */
    private PurchasingAccountsPayableItemAsset getSelectedLineItem(PurApLineForm purApLineForm) {
        PurchasingAccountsPayableDocument purApDoc = purApLineForm.getPurApDocs().get(purApLineForm.getActionPurApDocIndex());
        PurchasingAccountsPayableItemAsset selectedItem = purApDoc.getPurchasingAccountsPayableItemAssets().get(purApLineForm.getActionItemAssetIndex());
        if (!selectedItem.isActive()) {
            selectedItem = null;
        }
        return selectedItem;
    }


    /**
     * Get the user selected document.
     *
     * @param purApLineForm
     * @return
     */
    private PurchasingAccountsPayableDocument getSelectedPurApDoc(PurApLineForm purApLineForm) {
        return purApLineForm.getPurApDocs().get(purApLineForm.getActionPurApDocIndex());
    }

    /**
     * Handle apply payment action.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward applyPayment(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApForm = (PurApLineForm) form;
        PurchasingAccountsPayableItemAsset selectedLine = getSelectedLineItem(purApForm);

        if (selectedLine == null) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        Object question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
        if (question != null) {
            Object buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
            if ((CabConstants.TRADE_IN_INDICATOR_QUESTION.equals(question)) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                // create CAMS asset payment global document.
                return createApplyPaymentDocument(mapping, purApForm, selectedLine);
            } else {
                return mapping.findForward(KFSConstants.MAPPING_BASIC);
            }
        }
        if (selectedLine.isItemAssignedToTradeInIndicator()) {
            // TI indicator exists, bring up a warning message to confirm this action.
            return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.TRADE_IN_INDICATOR_QUESTION, SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(CabKeyConstants.QUESTION_TRADE_IN_INDICATOR_EXISTING), KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.APPLY_PAYMENT, "");
        }

        // create CAMS asset payment global document.
        return createApplyPaymentDocument(mapping, purApForm, selectedLine);
    }

    /**
     * Create CAMS asset payment document.
     *
     * @param mapping
     * @param purApForm
     * @param selectedLine
     * @param purApLineSession
     * @return
     * @throws WorkflowException
     */
    private ActionForward createApplyPaymentDocument(ActionMapping mapping, PurApLineForm purApForm, PurchasingAccountsPayableItemAsset selectedLine) throws WorkflowException {
        PurApLineSession purApLineSession = retrievePurApLineSession(purApForm);
        String documentNumber;
        // create CAMS asset payment global document.
        if ((documentNumber = purApLineDocumentService.processApplyPayment(selectedLine, purApForm.getPurApDocs(), purApLineSession, purApForm.getRequisitionIdentifier())) != null) {
            purApForm.setDocumentNumber(documentNumber);
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);

    }

    /**
     * Handle create asset action.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward createAsset(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PurApLineForm purApForm = (PurApLineForm) form;
        PurchasingAccountsPayableItemAsset selectedLine = getSelectedLineItem(purApForm);

        if (selectedLine == null) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        Object question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
        if (question != null) {
            Object buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
            if ((CabConstants.TRADE_IN_INDICATOR_QUESTION.equals(question)) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                // If PurAp user set capitalAssetNumbers for apply Asset Payment document, bring up a warning message for
                // confirmation.
                if (isSettingAssetsInPurAp(selectedLine)) {
                    return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.SKIP_ASSET_NUMBERS_TO_ASSET_GLOBAL_QUESTION, SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(CabKeyConstants.QUESTION_SKIP_ASSET_NUMBERS_TO_ASSET_GLOBAL), KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.CREATE_ASSET, "");
                } else {
                    return createAssetGlobalDocument(mapping, purApForm, selectedLine);
                }
            } else if (CabConstants.SKIP_ASSET_NUMBERS_TO_ASSET_GLOBAL_QUESTION.equals(question) && ConfirmationQuestion.YES.equals(buttonClicked)) {
                return createAssetGlobalDocument(mapping, purApForm, selectedLine);
            }
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        // validate selected line item
        validateCreateAssetAction(selectedLine);

        if (GlobalVariables.getMessageMap().hasNoErrors()) {
            // TI indicator exists, bring up a warning message to confirm this action.
            if (selectedLine.isItemAssignedToTradeInIndicator()) {
                return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.TRADE_IN_INDICATOR_QUESTION, SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(CabKeyConstants.QUESTION_TRADE_IN_INDICATOR_EXISTING), KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.CREATE_ASSET, "");
            }
            // If PurAp user set capitalAssetNumbers for apply Asset Payment document, bring up a warning message to confirm using
            // Asset Global document.
            else if (isSettingAssetsInPurAp(selectedLine)) {
                return this.performQuestionWithoutInput(mapping, form, request, response, CabConstants.SKIP_ASSET_NUMBERS_TO_ASSET_GLOBAL_QUESTION, SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(CabKeyConstants.QUESTION_SKIP_ASSET_NUMBERS_TO_ASSET_GLOBAL), KRADConstants.CONFIRMATION_QUESTION, CabConstants.Actions.CREATE_ASSET, "");
            } else {
                return createAssetGlobalDocument(mapping, purApForm, selectedLine);
            }
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * check if PurAp set CAMS Assets information
     *
     * @param selectedLine
     * @return
     */
    private boolean isSettingAssetsInPurAp(PurchasingAccountsPayableItemAsset selectedLine) {
        return selectedLine.getPurApItemAssets() != null && !selectedLine.getPurApItemAssets().isEmpty();
    }

    /**
     * Create asset global document
     *
     * @param mapping
     * @param purApForm
     * @param selectedLine
     * @param purApLineSession
     * @return
     * @throws WorkflowException
     */
    private ActionForward createAssetGlobalDocument(ActionMapping mapping, PurApLineForm purApForm, PurchasingAccountsPayableItemAsset selectedLine) throws WorkflowException {
        PurApLineSession purApLineSession = retrievePurApLineSession(purApForm);
        String documentNumber = null;
        // create CAMS asset global document.
        if ((documentNumber = purApLineDocumentService.processCreateAsset(selectedLine, purApForm.getPurApDocs(), purApLineSession, purApForm.getRequisitionIdentifier())) != null) {
            // forward link to asset global
            purApForm.setDocumentNumber(documentNumber);
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * Validate selected line item for asset global creation.
     *
     * @param selectedLine
     */
    protected void validateCreateAssetAction(PurchasingAccountsPayableItemAsset selectedLine) {
        KualiDecimal integerOne = new KualiDecimal(1);
        KualiDecimal quantity = selectedLine.getAccountsPayableItemQuantity();
        // check if item quantity is a fractional value greater than 1.
        if (quantity.isGreaterThan(integerOne) && quantity.mod(integerOne).isNonZero()) {
            GlobalVariables.getMessageMap().putError(CabPropertyConstants.PurApLineForm.PURAP_DOCS, CabKeyConstants.ERROR_FRACTIONAL_QUANTITY);
        }
        // if quantity is between (0,1) , set it to 1.
        else if (quantity.isGreaterThan(KualiDecimal.ZERO) && quantity.isLessThan(integerOne)) {
            selectedLine.setAccountsPayableItemQuantity(integerOne);
        }

    }

    protected ParameterService getParameterService() {
        return (ParameterService) SpringContext.getBean(ParameterService.class);
    }

    protected ConfigurationService getConfigurationService() {
        return SpringContext.getBean(ConfigurationService.class);
    }
}
