/*
 * 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.purap.document.validation.impl;

import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.MessageMap;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.purap.PurapConstants;
import org.kuali.kfs.module.purap.PurapConstants.ItemFields;
import org.kuali.kfs.module.purap.PurapKeyConstants;
import org.kuali.kfs.module.purap.PurapParameterConstants;
import org.kuali.kfs.module.purap.businessobject.PaymentRequestItem;
import org.kuali.kfs.module.purap.businessobject.PurApAccountingLine;
import org.kuali.kfs.module.purap.businessobject.PurApItem;
import org.kuali.kfs.module.purap.document.PaymentRequestDocument;
import org.kuali.kfs.module.purap.document.service.PurapService;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.validation.GenericValidation;
import org.kuali.kfs.sys.document.validation.Validation;
import org.kuali.kfs.sys.document.validation.event.AttributedDocumentEvent;
import org.kuali.kfs.sys.document.validation.impl.AccountingLineAmountPositiveValidation;
import org.kuali.kfs.sys.document.validation.impl.AccountingLineDataDictionaryValidation;
import org.kuali.kfs.sys.document.validation.impl.AccountingLineValuesAllowedValidationHutch;
import org.kuali.kfs.sys.document.validation.impl.BusinessObjectDataDictionaryValidation;
import org.kuali.kfs.sys.document.validation.impl.CompositeValidation;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.ArrayList;
import java.util.List;

public class PaymentRequestProcessItemValidation extends GenericValidation {

    private PurapService purapService;
    private PurApItem itemForValidation;
    private AttributedDocumentEvent event;
    private CompositeValidation reviewAccountingLineValidation;
    private PaymentRequestDocument preqDocument;
    private PurApAccountingLine preqAccountingLine;

    public boolean validate(AttributedDocumentEvent event) {
        boolean valid = true;
        this.event = event;

        PaymentRequestDocument paymentRequestDocument = (PaymentRequestDocument) event.getDocument();
        PaymentRequestItem preqItem = (PaymentRequestItem) itemForValidation;

        valid &= validateEachItem(paymentRequestDocument, preqItem);

        return valid;

    }

    /**
     * Calls another validate item method and passes an identifier string from the item.
     *
     * @param paymentRequestDocument - payment request document.
     * @param item
     * @return
     */
    protected boolean validateEachItem(PaymentRequestDocument paymentRequestDocument, PaymentRequestItem item) {
        boolean valid = true;
        String identifierString = item.getItemIdentifierString();
        valid &= validateItem(paymentRequestDocument, item, identifierString);
        return valid;
    }

    /**
     * Performs validation if full document entry not completed and peforms varying item validation.
     * Such as, above the line, items without accounts, items with accounts.
     *
     * @param paymentRequestDocument - payment request document
     * @param item                   - payment request item
     * @param identifierString       - identifier string used to mark in an error map
     * @return
     */
    public boolean validateItem(PaymentRequestDocument paymentRequestDocument, PaymentRequestItem item, String identifierString) {
        boolean valid = true;
        // only run item validations if before full entry
        if (!purapService.isFullDocumentEntryCompleted(paymentRequestDocument)) {
            if (item.getItemType().isLineItemIndicator()) {
                valid &= validateAboveTheLineItems(item, identifierString, paymentRequestDocument.isReceivingDocumentRequiredIndicator(), paymentRequestDocument);
            }
            valid &= validateItemWithoutAccounts(item, identifierString);
        }
        // always run account validations
        valid &= validateItemAccounts(paymentRequestDocument, item, identifierString);
        return valid;
    }

    /**
     * Validates above the line items.
     *
     * @param item                    - payment request item
     * @param identifierString        - identifier string used to mark in an error map
     * @param paymentRequestDocument, Payment Request Document for items
     * @return
     */
    protected boolean validateAboveTheLineItems(PaymentRequestItem item, String identifierString, boolean isReceivingDocumentRequiredIndicator, PaymentRequestDocument paymentRequestDocument) {
        boolean valid = true;
        // Currently Quantity is allowed to be NULL on screen;
        // must be either a positive number or NULL for DB
        MessageMap errorMap = GlobalVariables.getMessageMap();
        errorMap.clearErrorPath();

        if (ObjectUtils.isNotNull(item.getItemQuantity())) {
            if (item.getItemQuantity().isNegative()) {
                // if quantity is negative give an error
                valid = false;
                errorMap.putError(PurapConstants.ITEM_TAB_ERRORS, PurapKeyConstants.ERROR_ITEM_AMOUNT_BELOW_ZERO, ItemFields.INVOICE_QUANTITY, identifierString);
            }
            if (!isReceivingDocumentRequiredIndicator) {
                if (item.getPoOutstandingQuantity().isLessThan(item.getItemQuantity())) {
                    valid = false;
                    errorMap.putError(PurapConstants.ITEM_TAB_ERRORS, PurapKeyConstants.ERROR_ITEM_QUANTITY_TOO_MANY, ItemFields.INVOICE_QUANTITY, identifierString, ItemFields.OPEN_QUANTITY);
                }
            }
        }
        if (ObjectUtils.isNotNull(item.getExtendedPrice()) && item.getExtendedPrice().isPositive() && ObjectUtils.isNotNull(item.getPoOutstandingQuantity()) && item.getPoOutstandingQuantity().isPositive()) {

            // here we must require the user to enter some value for quantity if they want a credit amount associated
            if (ObjectUtils.isNull(item.getItemQuantity()) || item.getItemQuantity().isZero()) {
                // here we have a user not entering a quantity with an extended amount but the PO has a quantity...require user to
                // enter a quantity
                valid = false;
                errorMap.putError(PurapConstants.ITEM_TAB_ERRORS, PurapKeyConstants.ERROR_ITEM_QUANTITY_REQUIRED, ItemFields.INVOICE_QUANTITY, identifierString, ItemFields.OPEN_QUANTITY);
            }
        }
        //Modified to use the payment request document to not cause unnecessary refetch
        // check that non-quantity based items are not trying to pay on a zero encumbrance amount (check only prior to ap approval)
        if ((ObjectUtils.isNull(paymentRequestDocument.getPurapDocumentIdentifier())) || (PurapConstants.PaymentRequestStatuses.APPDOC_IN_PROCESS.equals(paymentRequestDocument.getApplicationDocumentStatus()))) {
// RICE20 : needed? :  !purapService.isFullDocumentEntryCompleted(item.getPaymentRequest())) {
            if ((item.getItemType().isAmountBasedGeneralLedgerIndicator()) && ((item.getExtendedPrice() != null) && item.getExtendedPrice().isNonZero())) {
                if (item.getPoOutstandingAmount() == null || item.getPoOutstandingAmount().isZero()) {
                    valid = false;
                    errorMap.putError(PurapConstants.ITEM_TAB_ERRORS, PurapKeyConstants.ERROR_ITEM_AMOUNT_ALREADY_PAID, identifierString);
                }
            }
        }

        return valid;
    }

    /**
     * Validates that the item must contain at least one account
     *
     * @param item - payment request item
     * @return
     */
    public boolean validateItemWithoutAccounts(PaymentRequestItem item, String identifierString) {
        boolean valid = true;
        if (ObjectUtils.isNotNull(item.getItemUnitPrice()) && (new KualiDecimal(item.getItemUnitPrice())).isNonZero() && item.isAccountListEmpty()) {
            valid = false;
            GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_ACCOUNTING_INCOMPLETE, identifierString);
        }
        return valid;
    }

    /**
     * Validates the totals for the item by account, that the total by each accounting line for the item, matches
     * the extended price on the item.
     *
     * @param paymentRequestDocument - payment request document
     * @param item                   - payment request item to validate
     * @param identifierString       - identifier string used to mark in an error map
     * @return
     */
    public boolean validateItemAccounts(PaymentRequestDocument paymentRequestDocument, PaymentRequestItem item, String identifierString) {
        boolean valid = true;
        List<PurApAccountingLine> accountingLines = item.getSourceAccountingLines();
        KualiDecimal itemTotal = item.getTotalAmount();
        KualiDecimal accountTotal = KualiDecimal.ZERO;
        for (PurApAccountingLine accountingLine : accountingLines) {
            if (accountingLine.getAmount().isZero()) {
                if (!canApproveAccountingLinesWithZeroAmount()) {
                    GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_ACCOUNTING_AMOUNT_INVALID, itemForValidation.getItemIdentifierString());
                    valid &= false;
                }
            }
            valid &= reviewAccountingLineValidation(paymentRequestDocument, accountingLine);
            accountTotal = accountTotal.add(accountingLine.getAmount());
        }
        if (purapService.isFullDocumentEntryCompleted(paymentRequestDocument)) {
            // check amounts not percent after full entry
            if (accountTotal.compareTo(itemTotal) != 0) {
                valid = false;
                GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_ACCOUNTING_AMOUNT_TOTAL, identifierString);
            }
        }
        return valid;
    }

    public CompositeValidation getReviewAccountingLineValidation() {
        return reviewAccountingLineValidation;
    }

    public void setReviewAccountingLineValidation(CompositeValidation reviewAccountingLineValidation) {
        this.reviewAccountingLineValidation = reviewAccountingLineValidation;
    }

    public PurapService getPurapService() {
        return purapService;
    }

    public void setPurapService(PurapService purapService) {
        this.purapService = purapService;
    }

    public PurApItem getItemForValidation() {
        return itemForValidation;
    }

    public void setItemForValidation(PurApItem itemForValidation) {
        this.itemForValidation = itemForValidation;
    }

    protected boolean reviewAccountingLineValidation(PaymentRequestDocument document, PurApAccountingLine accountingLine) {
        boolean valid = true;
        List<Validation> gauntlet = new ArrayList<Validation>();
        this.preqDocument = document;
        this.preqAccountingLine = accountingLine;

        createGauntlet(reviewAccountingLineValidation);

        for (Validation validation : gauntlet) {
            valid &= validation.validate(event);
        }

        return valid;
    }

    protected void createGauntlet(CompositeValidation validation) {
        for (Validation val : validation.getValidations()) {
            if (val instanceof CompositeValidation) {
                createGauntlet((CompositeValidation) val);
            } else if (val instanceof BusinessObjectDataDictionaryValidation) {
                addParametersToValidation((BusinessObjectDataDictionaryValidation) val);
            } else if (val instanceof AccountingLineAmountPositiveValidation) {
                addParametersToValidation((AccountingLineAmountPositiveValidation) val);
            } else if (val instanceof AccountingLineDataDictionaryValidation) {
                addParametersToValidation((AccountingLineDataDictionaryValidation) val);
            } else if (val instanceof AccountingLineValuesAllowedValidationHutch) {
                addParametersToValidation((AccountingLineValuesAllowedValidationHutch) val);
            } else {
                throw new IllegalStateException("Validations in the PaymentRequestProcessItemValidation must contain specific instances of validation");
            }
        }
    }

    /**
     * checks if an accounting line with zero dollar amount can be approved.  This will check
     * the system parameter APPROVE_ACCOUNTING_LINES_WITH_ZERO_DOLLAR_AMOUNT_IND and determines if the
     * line can be approved or not.
     *
     * @return true if the system parameter value is Y else returns N.
     */
    public boolean canApproveAccountingLinesWithZeroAmount() {
        boolean canApproveLine = false;

        // get parameter to see if accounting line with zero dollar amount can be approved.
        String approveZeroAmountLine = SpringContext.getBean(ParameterService.class).getParameterValueAsString(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.APPROVE_ACCOUNTING_LINES_WITH_ZERO_DOLLAR_AMOUNT_IND);

        if ("Y".equalsIgnoreCase(approveZeroAmountLine)) {
            return true;
        }

        return canApproveLine;
    }

    protected void addParametersToValidation(BusinessObjectDataDictionaryValidation validation) {
        validation.setBusinessObjectForValidation(this.preqAccountingLine);
    }

    protected void addParametersToValidation(AccountingLineAmountPositiveValidation validation) {
        validation.setAccountingDocumentForValidation(this.preqDocument);
        validation.setAccountingLineForValidation(this.preqAccountingLine);
    }

    protected void addParametersToValidation(AccountingLineDataDictionaryValidation validation) {
        validation.setAccountingLineForValidation(this.preqAccountingLine);
    }

    protected void addParametersToValidation(AccountingLineValuesAllowedValidationHutch validation) {
        validation.setAccountingDocumentForValidation(this.preqDocument);
        validation.setAccountingLineForValidation(this.preqAccountingLine);
    }

    /**
     * Gets the event attribute.
     *
     * @return Returns the event.
     */
    protected AttributedDocumentEvent getEvent() {
        return event;
    }

    /**
     * Sets the event attribute value.
     *
     * @param event The event to set.
     */
    protected void setEvent(AttributedDocumentEvent event) {
        this.event = event;
    }

    /**
     * Gets the preqDocument attribute.
     *
     * @return Returns the preqDocument.
     */
    protected PaymentRequestDocument getPreqDocument() {
        return preqDocument;
    }

    /**
     * Sets the preqDocument attribute value.
     *
     * @param preqDocument The preqDocument to set.
     */
    protected void setPreqDocument(PaymentRequestDocument preqDocument) {
        this.preqDocument = preqDocument;
    }

    /**
     * Gets the preqAccountingLine attribute.
     *
     * @return Returns the preqAccountingLine.
     */
    protected PurApAccountingLine getPreqAccountingLine() {
        return preqAccountingLine;
    }

    /**
     * Sets the preqAccountingLine attribute value.
     *
     * @param preqAccountingLine The preqAccountingLine to set.
     */
    protected void setPreqAccountingLine(PurApAccountingLine preqAccountingLine) {
        this.preqAccountingLine = preqAccountingLine;
    }

}
