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

import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.Organization;
import org.kuali.kfs.coa.businessobject.SubFundGroup;
import org.kuali.kfs.integration.cg.ContractsAndGrantsModuleService;
import org.kuali.kfs.integration.ld.LaborLedgerBalance;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.ec.EffortConstants;
import org.kuali.kfs.module.ec.EffortKeyConstants;
import org.kuali.kfs.module.ec.util.LedgerBalanceConsolidationHelper;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.MessageBuilder;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * The validator provides a set of facilities to determine whether the given ledger balances meet the specified requirements. As a
 * pattern, null would be returned if the requirements are met; otherwise, return an error message.
 */
public class LedgerBalanceFieldValidator {

    /**
     * check if the given ledger balance has an account qualified for effort reporting
     *
     * @param ledgerBalance the given ledger balance
     * @return null if the given ledger balance has an account qualified for effort reporting; otherwise, a message
     */
    public static Message hasValidAccount(LaborLedgerBalance ledgerBalance) {
        if (ObjectUtils.isNull(ledgerBalance.getAccount())) {
            String account = new StringBuilder(ledgerBalance.getChartOfAccountsCode()).append(EffortConstants.VALUE_SEPARATOR).append(ledgerBalance.getAccountNumber()).toString();
            return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_ACCOUNT_NUMBER_NOT_FOUND, account);
        }
        return null;
    }

    /**
     * detetermine if the fund group code associated with the given ledger balance is in the given fund group codes
     *
     * @param ledgerBalance  the given ledger balance
     * @param fundGroupCodes the given fund group codes
     * @return null if the fund group code associated with the given ledger balance is in the given fund group codes; otherwise, a
     * message
     */
    public static Message isInFundGroups(LaborLedgerBalance ledgerBalance, List<String> fundGroupCodes) {
        SubFundGroup subFundGroup = getSubFundGroup(ledgerBalance);

        if (ObjectUtils.isNull(subFundGroup) || !fundGroupCodes.contains(subFundGroup.getFundGroupCode())) {
            return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_FUND_GROUP_NOT_FOUND, subFundGroup.getFundGroupCode());
        }
        return null;
    }

    /**
     * detetermine if the sub fund group code associated with the given ledger balance is in the given sub fund group codes
     *
     * @param ledgerBalance     the given ledger balance
     * @param subFundGroupCodes the given sub fund group codes
     * @return null if the sub fund group code associated with the given ledger balance is in the given sub fund group codes;
     * otherwise, an error message
     */
    public static Message isInSubFundGroups(LaborLedgerBalance ledgerBalance, List<String> subFundGroupCodes) {
        SubFundGroup subFundGroup = getSubFundGroup(ledgerBalance);

        if (ObjectUtils.isNull(subFundGroup) || !subFundGroupCodes.contains(subFundGroup.getSubFundGroupCode())) {
            return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_FUND_GROUP_NOT_FOUND, subFundGroup.getSubFundGroupCode());
        }
        return null;
    }

    /**
     * determine if the total amount within the specified periods of the given ledger balance is ZERO
     *
     * @param ledgerBalance the given ledger balance
     * @param reportPeriods the specified periods
     * @return null the total amount within the specified periods of the given ledger balance is NOT ZERO; otherwise, a message
     * message
     */
    public static Message isNonZeroAmountBalanceWithinReportPeriod(LaborLedgerBalance ledgerBalance, Map<Integer, Set<String>> reportPeriods) {
        KualiDecimal totalAmount = LedgerBalanceConsolidationHelper.calculateTotalAmountWithinReportPeriod(ledgerBalance, reportPeriods);

        if (totalAmount.isZero()) {
            return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_ZERO_PAYROLL_AMOUNT, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * determine if the total amount within the specified periods of the given ledger balances is positive
     *
     * @param ledgerBalance the given ledger balance
     * @param reportPeriods the specified periods
     * @return null the total amount within the specified periods of the given ledger balance is positive; otherwise, a message
     * message
     */
    public static Message isTotalAmountPositive(Collection<LaborLedgerBalance> ledgerBalances, Map<Integer, Set<String>> reportPeriods) {
        KualiDecimal totalAmount = LedgerBalanceConsolidationHelper.calculateTotalAmountWithinReportPeriod(ledgerBalances, reportPeriods);

        if (!totalAmount.isPositive()) {
            return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_NONPOSITIVE_PAYROLL_AMOUNT, totalAmount.toString());
        }
        return null;
    }

    /**
     * check if there is at least one account of the given ledger balances that has a fund group code or subfund group code that is
     * in the specifed group codes. If fundGroupDenotesCGIndictor is ture, only examine the fund group code associated with the
     * ledger balances; otherwise, the sub fund group code.
     *
     * @param ledgerBalances the given ledger balances
     * @return null if one of the group codes associated with the ledger balances is in the specified codes; otherwise, a message
     * message
     */
    public static Message hasGrantAccount(Collection<LaborLedgerBalance> ledgerBalances) {
        for (LaborLedgerBalance balance : ledgerBalances) {
            if (balance.getAccount().isForContractsAndGrants()) {
                return null;
            }
        }

        return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_NOT_PAID_BY_GRANT_ACCOUNT, Message.TYPE_FATAL);
    }

    /**
     * determine whether there is at least one account of the given ledger balances that is funded by a federal grant. The award
     * associated with the account must be one of the given federal agency types or have an enabled federal pass through flag.
     *
     * @param the                    given labor ledger balances
     * @param federalAgencyTypeCodes the given federal agency type codes
     * @return null if there is at least one account with federal funding; otherwise, a message
     */
    public static Message hasFederalFunds(Collection<LaborLedgerBalance> ledgerBalances, Collection<String> federalAgencyTypeCodes) {
        for (LaborLedgerBalance balance : ledgerBalances) {
            Account account = balance.getAccount();
            if (SpringContext.getBean(ContractsAndGrantsModuleService.class).isAwardedByFederalAgency(account.getChartOfAccountsCode(), account.getAccountNumber(), federalAgencyTypeCodes)) {
                return null;
            }
        }
        return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_NOT_PAID_BY_FEDERAL_FUNDS, Message.TYPE_FATAL);
    }

    /**
     * determine if the given ledger balances have the accounts that belong to multiple organizations
     *
     * @param ledgerBalance the given ledger balance
     * @return null if the given ledger balances have the accounts that belong to a single organization; otherwise, a message
     */
    public static Message isFromSingleOrganization(Collection<LaborLedgerBalance> ledgerBalances) {
        Organization tempOrganization = null;

        boolean isFirstTime = true;
        for (LaborLedgerBalance balance : ledgerBalances) {
            Organization organization = balance.getAccount().getOrganization();

            if (isFirstTime) {
                tempOrganization = organization;
                isFirstTime = false;
            }

            if (!organization.equals(tempOrganization)) {
                return MessageBuilder.buildMessage(EffortKeyConstants.ERROR_MULTIPLE_ORGANIZATIONS_FOUND, Message.TYPE_FATAL);
            }
        }
        return null;
    }

    /**
     * get the sub fund group associated with the given ledger balance
     *
     * @param ledgerBalance the given ledger balance
     * @return the sub fund group associated with the given ledger balance
     */
    public static SubFundGroup getSubFundGroup(LaborLedgerBalance ledgerBalance) {
        SubFundGroup subFundGroup = null;
        try {
            subFundGroup = ledgerBalance.getAccount().getSubFundGroup();
        } catch (NullPointerException npe) {
            return null;
        }
        return subFundGroup;
    }
}
