/*
 * 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.fp.document.service.impl;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.fp.businessobject.CashDrawer;
import org.kuali.kfs.fp.businessobject.CashieringTransaction;
import org.kuali.kfs.fp.businessobject.CoinDetail;
import org.kuali.kfs.fp.businessobject.CurrencyDetail;
import org.kuali.kfs.fp.document.CashReceiptDocument;
import org.kuali.kfs.fp.document.dataaccess.CashManagementDao;
import org.kuali.kfs.fp.document.service.CashReceiptService;
import org.kuali.kfs.fp.service.CashDrawerService;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.kns.service.DictionaryValidationService;
import org.kuali.kfs.krad.bo.DocumentHeader;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSKeyConstants.CashReceipt;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.WorkflowDocumentFactory;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.location.api.campus.CampusService;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static org.kuali.kfs.sys.document.validation.impl.AccountingDocumentRuleBaseConstants.ERROR_PATH.DOCUMENT_ERROR_PREFIX;

/**
 * This is the default implementation of the CashReceiptService interface.
 */
@Transactional
public class CashReceiptServiceImpl implements CashReceiptService {
    org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CashReceiptServiceImpl.class);

    protected BusinessObjectService businessObjectService;
    protected CashManagementDao cashManagementDao;
    protected CashDrawerService cashDrawerService;
    protected DataDictionaryService dataDictionaryService;
    protected DictionaryValidationService dictionaryValidationService;

    /**
     * This method verifies the campus code provided exists.  This is done by retrieving all the available campuses from
     * the BusinessObjectService and then looking for a matching campus code within the result set.
     *
     * @param campusCode The campus code to be verified.
     * @return True if the campus code provided is valid and exists, false otherwise.
     */
    protected boolean verifyCampus(String campusCode) {
        return SpringContext.getBean(CampusService.class).getCampus(campusCode) != null;
    }


    /**
     * This method retrieves the cash receipt verification unit based on the user provided.  This is done by retrieving the campus
     * code associated with the user provided and then performing the lookup using this campus code.
     *
     * @param user The user to be used to retrieve the verification unit.
     * @return The cash receipt verification unit associated with the user provided.
     * @see org.kuali.kfs.fp.document.service.CashReceiptService#getCashReceiptVerificationUnit(org.kuali.rice.krad.bo.user.KualiUser)
     */
    @Override
    public String getCashReceiptVerificationUnitForUser(Person user) {
        String unitName = null;

        if (user == null) {
            throw new IllegalArgumentException("invalid (null) user");
        }

        return user.getCampusCode();
    }


    /**
     * This method retrieves a collection of cash receipts using the verification unit and the status provided to
     * retrieve the cash receipts.
     *
     * @param verificationUnit The verification unit used to retrieve a collection of associated cash receipts.
     * @param statusCode       The status code of the cash receipts to be retrieved.
     * @return A collection of cash receipt documents which match the search criteria provided.
     * @see org.kuali.kfs.fp.document.service.CashReceiptService#getCashReceipts(java.lang.String, java.lang.String)
     */
    @Override
    public List getCashReceipts(String verificationUnit, String statusCode) {
        if (StringUtils.isBlank(statusCode)) {
            throw new IllegalArgumentException("invalid (blank) statusCode");
        }

        String[] statii = new String[]{statusCode};
        return getCashReceipts(verificationUnit, statii);
    }

    /**
     * This method retrieves a collection of cash receipts using the verification unit and the statuses provided to
     * retrieve the cash receipts.
     *
     * @param verificationUnit The verification unit used to retrieve a collection of associated cash receipts.
     * @param statii           A collection of possible statuses that will be used in the lookup of cash receipts.
     * @return A collection of cash receipt documents which match the search criteria provided.
     * @see org.kuali.kfs.fp.document.service.CashReceiptService#getCashReceipts(java.lang.String, java.lang.String[])
     */
    @Override
    public List getCashReceipts(String verificationUnit, String[] statii) {
        if (StringUtils.isBlank(verificationUnit)) {
            throw new IllegalArgumentException("invalid (blank) verificationUnit");
        }
        if (statii == null) {
            throw new IllegalArgumentException("invalid (null) statii");
        } else {
            if (statii.length == 0) {
                throw new IllegalArgumentException("invalid (empty) statii");
            } else {
                for (int i = 0; i < statii.length; ++i) {
                    if (StringUtils.isBlank(statii[i])) {
                        throw new IllegalArgumentException("invalid (blank) status code " + i);
                    }
                }
            }
        }

        return getPopulatedCashReceipts(verificationUnit, statii);
    }

    /**
     * This method retrieves a populated collection of cash receipts using the lookup parameters provided.  A populated
     * cash receipt document is a cash receipt document with fully populated workflow fields.
     *
     * @param verificationUnit The verification unit used to retrieve a collection of associated cash receipts.
     * @param statii           A collection of possible statuses that will be used in the lookup of the cash receipts.
     * @return List of CashReceiptDocument instances with their associated workflowDocuments populated.
     */
    public List<CashReceiptDocument> getPopulatedCashReceipts(String verificationUnit, String[] statii) {
        Map queryCriteria = buildCashReceiptCriteriaMap(verificationUnit, statii);

        List<CashReceiptDocument> documents = new ArrayList<CashReceiptDocument>(businessObjectService.findMatchingOrderBy(CashReceiptDocument.class, queryCriteria, KFSPropertyConstants.DOCUMENT_NUMBER, true));

        populateWorkflowFields(documents);

        return documents;
    }


    /**
     * This method builds out a map of search criteria for performing cash receipt lookups using the values provided.
     *
     * @param campusCode The campus code to use as search criteria for looking up cash receipts.
     * @param statii     A collection of possible statuses to use as search criteria for looking up cash receipts.
     * @return The search criteria provided in a map with CashReceiptConstants used as keys to the parameters given.
     */
    protected Map buildCashReceiptCriteriaMap(String campusCode, String[] statii) {
        Map queryCriteria = new HashMap();

        if (statii.length == 1) {
            queryCriteria.put(KFSConstants.CashReceiptConstants.CASH_RECEIPT_DOC_HEADER_STATUS_CODE_PROPERTY_NAME, statii[0]);
        } else if (statii.length > 0) {
            List<String> statusList = Arrays.asList(statii);
            queryCriteria.put(KFSConstants.CashReceiptConstants.CASH_RECEIPT_DOC_HEADER_STATUS_CODE_PROPERTY_NAME, statusList);
        }

        queryCriteria.put(KFSConstants.CashReceiptConstants.CASH_RECEIPT_CAMPUS_LOCATION_CODE_PROPERTY_NAME, campusCode);

        return queryCriteria;
    }

    /**
     * This method populates the workflowDocument field of each CashReceiptDocument in the given List
     *
     * @param documents A collection of CashReceiptDocuments to be populated with workflow document data.
     */
    protected void populateWorkflowFields(List documents) {
        for (Iterator i = documents.iterator(); i.hasNext(); ) {
            CashReceiptDocument cr = (CashReceiptDocument) i.next();
            DocumentHeader docHeader = cr.getDocumentHeader();
            WorkflowDocument workflowDocument = WorkflowDocumentFactory.loadDocument(GlobalVariables.getUserSession().getPrincipalId(), docHeader.getDocumentNumber());

            docHeader.setWorkflowDocument(workflowDocument);
        }
    }

    /**
     * This method retrieves the cash details from the cash receipt document provided and adds those details to the
     * associated cash drawer.  After the details are added to the drawer, the drawer is persisted to the database.
     *
     * @param crDoc The cash receipt document the cash details will be retrieved from.
     * @see org.kuali.kfs.fp.document.service.CashReceiptService#addCashDetailsToCashDrawer(org.kuali.kfs.fp.document.CashReceiptDocument)
     */
    @Override
    public void addCashDetailsToCashDrawer(CashReceiptDocument crDoc) {
        CashDrawer drawer = retrieveCashDrawer(crDoc);
        // we need to to add the currency and coin to the cash management doc's cumulative CR as well
        if (crDoc.getCurrencyDetail() != null && !crDoc.getCurrencyDetail().isEmpty()) {
            CurrencyDetail cumulativeCurrencyDetail = cashManagementDao.findCurrencyDetailByCashieringStatus(drawer.getReferenceFinancialDocumentNumber(), CashieringTransaction.DETAIL_DOCUMENT_TYPE, KFSConstants.CurrencyCoinSources.CASH_RECEIPTS);
            cumulativeCurrencyDetail.add(crDoc.getConfirmedCurrencyDetail());
            cumulativeCurrencyDetail.subtract(crDoc.getConfirmedChangeCurrencyDetail());
            businessObjectService.save(cumulativeCurrencyDetail);
            drawer.addCurrency(crDoc.getConfirmedCurrencyDetail());
            drawer.removeCurrency(crDoc.getConfirmedChangeCurrencyDetail());
        }
        if (crDoc.getCoinDetail() != null && !crDoc.getCoinDetail().isEmpty()) {
            CoinDetail cumulativeCoinDetail = cashManagementDao.findCoinDetailByCashieringStatus(drawer.getReferenceFinancialDocumentNumber(), CashieringTransaction.DETAIL_DOCUMENT_TYPE, KFSConstants.CurrencyCoinSources.CASH_RECEIPTS);
            cumulativeCoinDetail.add(crDoc.getConfirmedCoinDetail());
            cumulativeCoinDetail.subtract(crDoc.getConfirmedChangeCoinDetail());
            businessObjectService.save(cumulativeCoinDetail);
            drawer.addCoin(crDoc.getConfirmedCoinDetail());
            drawer.removeCoin(crDoc.getConfirmedChangeCoinDetail());
        }
        businessObjectService.save(drawer);
    }

    /**
     * This method finds the appropriate cash drawer for this cash receipt document to add cash to.
     *
     * @param crDoc The document the cash drawer will be retrieved from.
     * @return An instance of a cash drawer associated with the cash receipt document provided.
     */
    protected CashDrawer retrieveCashDrawer(CashReceiptDocument crDoc) {
        String campusCode = crDoc.getCampusLocationCode();
        if (campusCode == null) {
            throw new RuntimeException("Cannot find workgroup name for Cash Receipt document: " + crDoc.getDocumentNumber());
        }

        CashDrawer drawer = cashDrawerService.getByCampusCode(campusCode);
        if (drawer == null) {
            throw new RuntimeException("There is no Cash Drawer for Workgroup " + campusCode);
        }
        return drawer;
    }

    /**
     * @see org.kuali.module.financial.service.CashReceiptTotalsVerificationService#areCashTotalsInvalid(org.kuali.kfs.fp.document.CashReceiptDocument)
     */
    @Override
    @Deprecated
    public boolean areCashTotalsInvalid(CashReceiptDocument cashReceiptDocument) {
        return areCashAmountsInvalid(cashReceiptDocument);
    }

    /**
     * @see org.kuali.module.financial.service.CashReceiptTotalsVerificationService#areCashTotalsInvalid(org.kuali.kfs.fp.document.CashReceiptDocument)
     */
    @Override
    public boolean areCashAmountsInvalid(CashReceiptDocument cashReceiptDocument) {
        /*
         * We need to make sure that amount of each denomination in any currency or coin detail is non-negative, not just the total.
         * The previous code didn't consider whether check/cash/change have been confirmed or not, when checking the amounts are checked.
         * We need to distinguish the confirmation status here, because when CR is routed, Cash Manger can adjust these amounts before
         * approval, so the confirmed amounts shall be checked; otherwise, if CR is preroute, we should check the original amounts.
         */

        boolean isInvalid = false;
        isInvalid |= isDetailOrTotalInvalid(cashReceiptDocument, KFSPropertyConstants.CHECK);
        isInvalid |= isDetailOrTotalInvalid(cashReceiptDocument, KFSPropertyConstants.CURRENCY);
        isInvalid |= isDetailOrTotalInvalid(cashReceiptDocument, KFSPropertyConstants.COIN);
        isInvalid |= isDetailOrTotalInvalid(cashReceiptDocument, KFSPropertyConstants.CHANGE_CURRENCY);
        isInvalid |= isDetailOrTotalInvalid(cashReceiptDocument, KFSPropertyConstants.CHANGE_COIN);
        return isInvalid;
    }

    /**
     * Returns true if any cash detail or total amount of the specified cash category is invalid,
     * and puts an error message in the error map for the corresponding detail/total property.
     *
     * @param cashReceiptDocument submitted cash receipt document
     * @param cashCategory        cash category (i.e. check, currency, coin)
     * @return true if any amount of the category is an invalid value
     */
    protected boolean isDetailOrTotalInvalid(CashReceiptDocument cashReceiptDocument, String cashCategory) {
        boolean isInvalid = false;
        boolean detailNegative = false;
        boolean totalNegative = false;
        KualiDecimal totalAmount = null;

        boolean isPreRoute = cashReceiptDocument.isPreRoute();
        String detailPrefix = isPreRoute ? cashCategory : KFSPropertyConstants.CONFIRMED + StringUtils.capitalize(cashCategory);
        String detailProperty = DOCUMENT_ERROR_PREFIX + detailPrefix + KFSPropertyConstants.DETAIL_PREFIX + KFSPropertyConstants.TOTAL_AMOUNT;
        String totalProperty = DOCUMENT_ERROR_PREFIX + KFSPropertyConstants.TOTAL + StringUtils.capitalize(detailPrefix) + StringUtils.capitalize(KFSPropertyConstants.AMOUNT);
        String detailErrorProperty = KFSPropertyConstants.TOTAL_AMOUNT;
        String totalErrorProperty = KFSPropertyConstants.TOTAL + StringUtils.capitalize(cashCategory) + StringUtils.capitalize(KFSPropertyConstants.AMOUNT);
        String detailErrorLabel = "";
        String totalErrorLabel = dataDictionaryService.getAttributeErrorLabel(CashReceiptDocument.class, totalErrorProperty);
        totalErrorLabel = isPreRoute ? totalErrorLabel : StringUtils.capitalize(KFSPropertyConstants.CONFIRMED) + totalErrorLabel;

        if (StringUtils.equalsIgnoreCase(KFSPropertyConstants.CHECK, cashCategory)) {
            totalAmount = isPreRoute ? cashReceiptDocument.getTotalCheckAmount() : cashReceiptDocument.getTotalConfirmedCheckAmount();
            // we don't validate each check here, since that should have been validated when checks are added
        } else if (StringUtils.equalsIgnoreCase(KFSPropertyConstants.CURRENCY, cashCategory)) {
            CurrencyDetail currencyDetail = isPreRoute ? cashReceiptDocument.getCurrencyDetail() : cashReceiptDocument.getConfirmedCurrencyDetail();
            detailNegative = ObjectUtils.isNull(currencyDetail) ? false : currencyDetail.hasNegativeAmount();
            totalAmount = isPreRoute ? cashReceiptDocument.getTotalCurrencyAmount() : cashReceiptDocument.getTotalConfirmedCurrencyAmount();
            detailErrorLabel = dataDictionaryService.getAttributeErrorLabel(CurrencyDetail.class, detailErrorProperty);
        } else if (StringUtils.equalsIgnoreCase(KFSPropertyConstants.COIN, cashCategory)) {
            CoinDetail coinDetail = isPreRoute ? cashReceiptDocument.getCoinDetail() : cashReceiptDocument.getConfirmedCoinDetail();
            detailNegative = ObjectUtils.isNull(coinDetail) ? false : coinDetail.hasNegativeAmount();
            totalAmount = isPreRoute ? cashReceiptDocument.getTotalCoinAmount() : cashReceiptDocument.getTotalConfirmedCoinAmount();
            detailErrorLabel = dataDictionaryService.getAttributeErrorLabel(CoinDetail.class, detailErrorProperty);
        } else if (StringUtils.equalsIgnoreCase(KFSPropertyConstants.CHANGE_CURRENCY, cashCategory)) {
            CurrencyDetail currencyDetail = isPreRoute ? cashReceiptDocument.getChangeCurrencyDetail() : cashReceiptDocument.getConfirmedChangeCurrencyDetail();
            detailNegative = ObjectUtils.isNull(currencyDetail) ? false : currencyDetail.hasNegativeAmount();
            totalAmount = isPreRoute ? cashReceiptDocument.getTotalChangeCurrencyAmount() : cashReceiptDocument.getTotalConfirmedChangeCurrencyAmount();
            detailErrorLabel = dataDictionaryService.getAttributeErrorLabel(CurrencyDetail.class, detailErrorProperty);
        } else if (StringUtils.equalsIgnoreCase(KFSPropertyConstants.CHANGE_COIN, cashCategory)) {
            CoinDetail coinDetail = isPreRoute ? cashReceiptDocument.getChangeCoinDetail() : cashReceiptDocument.getConfirmedChangeCoinDetail();
            detailNegative = ObjectUtils.isNull(coinDetail) ? false : coinDetail.hasNegativeAmount();
            totalAmount = isPreRoute ? cashReceiptDocument.getTotalChangeCoinAmount() : cashReceiptDocument.getTotalConfirmedChangeCoinAmount();
            detailErrorLabel = dataDictionaryService.getAttributeErrorLabel(CoinDetail.class, detailErrorProperty);
        }

        totalNegative = (totalAmount == null) ? false : totalAmount.isNegative();
        detailErrorLabel = isPreRoute ? detailErrorLabel : StringUtils.capitalize(KFSPropertyConstants.CONFIRMED) + detailErrorLabel;

        // if cash detail has negative amount(s), put error message on its total amount field in detail tab
        if (detailNegative) {
            GlobalVariables.getMessageMap().putError(detailProperty, CashReceipt.ERROR_NEGATIVE_CASH_DETAIL_AMOUNT, detailErrorLabel);
            isInvalid = true;
        }

        // if cash total amount is negative, put error message on its total amount field in reconciliation tab
        if (totalNegative) {
            GlobalVariables.getMessageMap().putError(totalProperty, CashReceipt.ERROR_NEGATIVE_TOTAL, totalErrorLabel);
            isInvalid = true;
        } else {
            int precount = GlobalVariables.getMessageMap().getNumberOfPropertiesWithErrors();
            // only run DD validation on the total amount if it's define in the DD
            if (dataDictionaryService.isAttributeDefined(CashReceiptDocument.class, totalProperty)) {
                dictionaryValidationService.validateDocumentAttribute(cashReceiptDocument, totalProperty, DOCUMENT_ERROR_PREFIX);
            }
            int postcount = GlobalVariables.getMessageMap().getNumberOfPropertiesWithErrors();
            if (postcount > precount) {
                // replace generic error message, if any, with something more readable
                GlobalVariables.getMessageMap().replaceError(totalProperty, KFSKeyConstants.ERROR_MAX_LENGTH, CashReceipt.ERROR_EXCESSIVE_TOTAL, totalErrorLabel);
                isInvalid = true;
            }
        }

        return isInvalid;
    }

    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    public void setCashManagementDao(CashManagementDao cashManagementDao) {
        this.cashManagementDao = cashManagementDao;
    }

    public void setCashDrawerService(CashDrawerService cashDrawerService) {
        this.cashDrawerService = cashDrawerService;
    }

    public void setDictionaryValidationService(DictionaryValidationService dictionaryValidationService) {
        this.dictionaryValidationService = dictionaryValidationService;
    }

    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
        this.dataDictionaryService = dataDictionaryService;
    }

}

