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

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.AccountGlobalDetail;
import org.kuali.kfs.coa.businessobject.SubObjectCodeGlobal;
import org.kuali.kfs.coa.businessobject.SubObjectCodeGlobalDetail;
import org.kuali.kfs.kns.document.MaintenanceDocument;
import org.kuali.kfs.krad.bo.PersistableBusinessObject;
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.KFSPropertyConstants;

import java.util.List;

/**
 * This class implements the business rules specific to the {@link SubObjCdGlobal} Maintenance Document.
 */
public class SubObjCdGlobalRule extends GlobalDocumentRuleBase {
    protected SubObjectCodeGlobal subObjCdGlobal;

    /**
     * This method sets the convenience objects like subObjCdGlobal and all the detail objects, so you have short and easy handles to the new and
     * old objects contained in the maintenance document. It also calls the BusinessObjectBase.refresh(), which will attempt to load
     * all sub-objects from the DB by their primary keys, if available. This also loops through each detail item (SubObjCdGlobalDetail and AccountGlobalDetail)
     * are refreshed
     *
     * @param document - the maintenanceDocument being evaluated
     */
    @Override
    public void setupConvenienceObjects() {

        // setup subObjCdGlobal convenience objects,
        // make sure all possible sub-objects are populated
        subObjCdGlobal = (SubObjectCodeGlobal) super.getNewBo();

        // forces refreshes on all the sub-objects in the lists
        for (SubObjectCodeGlobalDetail objectCodeGlobalDetail : subObjCdGlobal.getSubObjCdGlobalDetails()) {
            objectCodeGlobalDetail.refreshNonUpdateableReferences();
        }
        for (AccountGlobalDetail accountGlobalDetail : subObjCdGlobal.getAccountGlobalDetails()) {
            accountGlobalDetail.refreshNonUpdateableReferences();
        }
    }

    /**
     * This performs rules checks on document approve
     * <ul>
     * <li>{@link SubObjCdGlobalRule#checkSimpleRulesAllLines()}</li>
     * </ul>
     * This rule fails on business rule failures
     *
     * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomApproveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
     */
    @Override
    protected boolean processCustomApproveDocumentBusinessRules(MaintenanceDocument document) {
        boolean success = true;
        setupConvenienceObjects();
        // check simple rules
        success &= checkSimpleRulesAllLines();

        success &= checkOnlyOneChartErrorWrapper(subObjCdGlobal.getAccountGlobalDetails());

        return success;
    }

    /**
     * This performs rules checks on document route
     * <ul>
     * <li>{@link SubObjCdGlobalRule#checkSimpleRulesAllLines()}</li>
     * </ul>
     * This rule fails on business rule failures
     *
     * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
     */
    @Override
    protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
        boolean success = true;
        setupConvenienceObjects();
        // check simple rules
        success &= checkSimpleRulesAllLines();

        success &= checkAccountDetails(subObjCdGlobal.getAccountGlobalDetails());

        return success;
    }

    /**
     * This performs rules checks on document save
     * <ul>
     * <li>{@link SubObjCdGlobalRule#checkSimpleRulesAllLines()}</li>
     * </ul>
     * This rule does not fail on business rule failures
     *
     * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
     */
    @Override
    protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
        setupConvenienceObjects();
        // check simple rules
        checkSimpleRulesAllLines();

        return true;
    }

    /**
     * Before adding either a {@link AccountGlobalDetail} or {@link SubObjCdGlobalDetail} this checks to make sure
     * that the account and chart are filled in, in the case of SubObjCdGlobalDetail it also checks
     * that the object code and fiscal year are filled in
     * If any of these fail, it fails to add the new line to the collection
     *
     * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomAddCollectionLineBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument, java.lang.String, org.kuali.rice.krad.bo.PersistableBusinessObject)
     */
    public boolean processCustomAddCollectionLineBusinessRules(MaintenanceDocument document, String collectionName, PersistableBusinessObject bo) {
        boolean success = true;
        if (bo instanceof AccountGlobalDetail) {
            AccountGlobalDetail detail = (AccountGlobalDetail) bo;
            // make sure that both primary keys are available for this object
            if (!checkEmptyValue(detail.getAccountNumber())) {
                // put an error about accountnumber
                GlobalVariables.getMessageMap().putError("accountNumber", KFSKeyConstants.ERROR_REQUIRED, "Account Number");
                success &= false;
            }
            if (!checkEmptyValue(detail.getChartOfAccountsCode())) {
                // put an error about chart code
                GlobalVariables.getMessageMap().putError("chartOfAccountsCode", KFSKeyConstants.ERROR_REQUIRED, "Chart of Accounts Code");
                success &= false;
            }
            success &= checkAccountDetails(detail);
        } else if (bo instanceof SubObjectCodeGlobalDetail) {
            SubObjectCodeGlobalDetail detail = (SubObjectCodeGlobalDetail) bo;
            if (!checkEmptyValue(detail.getChartOfAccountsCode())) {
                // put an error about accountnumber
                GlobalVariables.getMessageMap().putError("chartOfAccountsCode", KFSKeyConstants.ERROR_REQUIRED, "Chart of Accounts Code");
                success &= false;
            }
            if (!checkEmptyValue(detail.getFinancialObjectCode())) {
                // put an error about financial object code
                GlobalVariables.getMessageMap().putError("financialObjectCode", KFSKeyConstants.ERROR_REQUIRED, "Financial Object Code");
                success &= false;
            }
            if (!checkEmptyValue(detail.getUniversityFiscalYear())) {
                // put an error about financial object code
                GlobalVariables.getMessageMap().putError("universityFiscalYear", KFSKeyConstants.ERROR_REQUIRED, "University Fiscal Year");
                success &= false;
            }
            success &= checkSubObjectCodeDetails(detail);
        }
        return success;
    }

    /**
     * This calls the {@link SubObjCdGlobalRule#checkAccountDetails(AccountGlobalDetail)} on each AccountGlobalDetail as well as calling
     * {@link SubObjCdGlobalRule#checkOnlyOneChartErrorWrapper(List)} to ensure there is just one chart
     *
     * @param details
     * @return false if any of the detail objects fail they're sub-rule
     */
    public boolean checkAccountDetails(List<AccountGlobalDetail> details) {
        boolean success = true;

        // check if there are any accounts
        if (details.size() == 0) {
            putFieldError(KFSConstants.MAINTENANCE_ADD_PREFIX + "accountGlobalDetails.accountNumber", KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_ACCOUNT_NO_ACCOUNTS);
            success = false;
        } else {
            // check each account
            int index = 0;
            for (AccountGlobalDetail dtl : details) {
                String errorPath = MAINTAINABLE_ERROR_PREFIX + "accountGlobalDetails[" + index + "]";
                GlobalVariables.getMessageMap().addToErrorPath(errorPath);
                success &= checkAccountDetails(dtl);
                GlobalVariables.getMessageMap().removeFromErrorPath(errorPath);
                index++;
            }
            success &= checkOnlyOneChartErrorWrapper(details);
        }

        return success;
    }

    /**
     * This checks that if the account and chart are entered that the  account associated with the AccountGlobalDetail is valid
     *
     * @param dtl - the AccountGlobalDetail we are dealing with
     * @return false if any of the fields are found to be invalid
     */
    public boolean checkAccountDetails(AccountGlobalDetail dtl) {
        boolean success = true;
        int originalErrorCount = GlobalVariables.getMessageMap().getErrorCount();
        getDictionaryValidationService().validateBusinessObject(dtl);
        if (StringUtils.isNotBlank(dtl.getAccountNumber()) && StringUtils.isNotBlank(dtl.getChartOfAccountsCode())) {
            dtl.refreshReferenceObject("account");
            if (ObjectUtils.isNull(dtl.getAccount())) {
                GlobalVariables.getMessageMap().putError("accountNumber", KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_ACCOUNT_INVALID_ACCOUNT, new String[]{dtl.getChartOfAccountsCode(), dtl.getAccountNumber()});
            }
        }
        success &= GlobalVariables.getMessageMap().getErrorCount() == originalErrorCount;

        return success;
    }

    /**
     * This checks that if the object code, chart code, and fiscal year are entered it is a valid Object Code, chart, and Fiscal Year
     * associated with this SubObjectCode
     *
     * @param dtl - the SubObjCdGlobalDetail we are checking
     * @return false if any of the fields are found to be invalid
     */
    public boolean checkSubObjectCodeDetails(SubObjectCodeGlobalDetail dtl) {
        boolean success = true;
        int originalErrorCount = GlobalVariables.getMessageMap().getErrorCount();
        getDictionaryValidationService().validateBusinessObject(dtl);
        if (StringUtils.isNotBlank(dtl.getFinancialObjectCode()) && StringUtils.isNotBlank(dtl.getChartOfAccountsCode()) && dtl.getUniversityFiscalYear() > 0) {
            dtl.refreshReferenceObject("financialObject");
            dtl.refreshReferenceObject("universityFiscal");
            dtl.refreshReferenceObject("chartOfAccounts");
            if (ObjectUtils.isNull(dtl.getChartOfAccounts()) || ObjectUtils.isNull(dtl.getUniversityFiscal()) || ObjectUtils.isNull(dtl.getFinancialObject())) {
                GlobalVariables.getMessageMap().putError("financialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_INVALID_OBJECT_CODE, new String[]{dtl.getFinancialObjectCode(), dtl.getChartOfAccountsCode(), dtl.getUniversityFiscalYear().toString()});
            }
        }
        success &= GlobalVariables.getMessageMap().getErrorCount() == originalErrorCount;

        return success;
    }

    /**
     * This method checks the simple rules for all lines at once and gets called on save, submit, etc. but not on add
     * <ul>
     * <li>{@link SubObjCdGlobalRule#checkForSubObjCdGlobalDetails(List)}</li>
     * <li>{@link SubObjCdGlobalRule#checkForAccountGlobalDetails(List)}</li>
     * <li>{@link SubObjCdGlobalRule#checkFiscalYearAllLines(SubObjCdGlobal)}</li>
     * <li>{@link SubObjCdGlobalRule#checkChartAllLines(SubObjCdGlobal)}</li>
     * </ul>
     *
     * @return
     */
    protected boolean checkSimpleRulesAllLines() {
        boolean success = true;
        // check if there are any object codes and accounts, if either fails this should fail
        if (!checkForSubObjCdGlobalDetails(subObjCdGlobal.getSubObjCdGlobalDetails()) && !checkForAccountGlobalDetails(subObjCdGlobal.getAccountGlobalDetails())) {
            success = false;
        } else {
            // check object codes
            success &= checkFiscalYearAllLines(subObjCdGlobal);

            // check chart code
            success &= checkChartAllLines(subObjCdGlobal);

        }
        return success;
    }

    /**
     * This checks that the SubObjCdGlobalDetail list isn't empty or null
     *
     * @param subObjCdGlobalDetails
     * @return false if the list is null or empty
     */
    protected boolean checkForSubObjCdGlobalDetails(List<SubObjectCodeGlobalDetail> subObjCdGlobalDetails) {
        if (subObjCdGlobalDetails == null || subObjCdGlobalDetails.size() == 0) {
            putFieldError(KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "." + KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_NO_OBJECT_CODE);
            return false;
        }
        return true;
    }

    /**
     * This checks that the AccountGlobalDetail list isn't empty or null
     *
     * @param acctChangeDetails
     * @return false if the list is null or empty
     */
    protected boolean checkForAccountGlobalDetails(List<AccountGlobalDetail> acctChangeDetails) {
        if (acctChangeDetails == null || acctChangeDetails.size() == 0) {
            putFieldError(KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.ACCOUNT_CHANGE_DETAILS + "." + KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_NO_ACCOUNT);
            return false;
        }
        return true;
    }

    /**
     * This checks that the fiscal year is the same on the doc and all SubObjCdGlobalDetails
     *
     * @param socChangeDocument
     * @return false if the fiscal year is not the same on the doc and any of the SubObjCdGlobalDetails
     */
    protected boolean checkFiscalYearAllLines(SubObjectCodeGlobal socChangeDocument) {
        boolean success = true;
        int i = 0;
        for (SubObjectCodeGlobalDetail subObjCdGlobal : socChangeDocument.getSubObjCdGlobalDetails()) {

            // check fiscal year first
            success &= checkFiscalYear(socChangeDocument, subObjCdGlobal, i, false);

            // increment counter for sub object changes list
            i++;
        }

        return success;
    }

    /**
     * This checks that the chart is the same on the document, SubObjCdGlobalDetails and AccountGlobalDetails
     *
     * @param socChangeDocument
     * @return false if the chart is missing or not the same on the doc, or the detail lists
     */
    protected boolean checkChartAllLines(SubObjectCodeGlobal socChangeDocument) {
        boolean success = true;
        int i = 0;
        for (SubObjectCodeGlobalDetail subObjCdGlobal : socChangeDocument.getSubObjCdGlobalDetails()) {

            // check chart
            success &= checkChartOnSubObjCodeDetails(socChangeDocument, subObjCdGlobal, i, false);
            // increment counter for sub object changes list
            i++;
        }

        // check each account change
        i = 0;
        for (AccountGlobalDetail acctChangeDetail : socChangeDocument.getAccountGlobalDetails()) {

            // check chart
            success &= checkChartOnAccountDetails(socChangeDocument, acctChangeDetail, i, false);
            // increment counter for account changes list
            i++;
        }

        return success;
    }

    /**
     * This checks to make sure that the fiscal year on the {@link SubObjCdGlobalDetail} is not empty and
     * the document's fiscal year matches the detail's fiscal year
     *
     * @param socChangeDocument
     * @return false if the fiscal year is missing or is not the same between the doc and the detail
     */
    protected boolean checkFiscalYear(SubObjectCodeGlobal socChangeDocument, SubObjectCodeGlobalDetail socChangeDetail, int lineNum, boolean add) {
        boolean success = true;
        String errorPath = KFSConstants.EMPTY_STRING;
        // first must have an actual fiscal year
        if (ObjectUtils.isNull(socChangeDetail.getUniversityFiscal())) {
            if (add) {
                errorPath = KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "." + "universityFiscalYear";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_FISCAL_YEAR_MUST_EXIST);
            } else {
                errorPath = KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "[" + lineNum + "]." + "universityFiscalYear";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_FISCAL_YEAR_MUST_EXIST);
            }
            success &= false;
            return success;
        }

        // the two fiscal years from the document and detail must match
        if (!socChangeDocument.getUniversityFiscal().equals(socChangeDetail.getUniversityFiscal())) {
            if (add) {
                errorPath = KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "." + "universityFiscalYear";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_FISCAL_YEAR_MUST_BE_SAME);
            } else {
                errorPath = KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "[" + lineNum + "]." + "universityFiscalYear";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_FISCAL_YEAR_MUST_BE_SAME);
            }
            success &= false;
            return success;
        }

        return success;
    }

    /**
     * This checks to make sure that the chart of accounts on the {@link SubObjCdGlobalDetail} is not empty and
     * the document's chart matches the detail's chart
     *
     * @param socChangeDocument
     * @param socChangeDetail
     * @param lineNum
     * @param add
     * @return false if the chart is missing or is not the same between the doc and the detail
     */
    protected boolean checkChartOnSubObjCodeDetails(SubObjectCodeGlobal socChangeDocument, SubObjectCodeGlobalDetail socChangeDetail, int lineNum, boolean add) {
        boolean success = true;
        String errorPath = KFSConstants.EMPTY_STRING;

        if (StringUtils.isBlank(socChangeDetail.getChartOfAccountsCode())) {
            return success; // just return, the existence check will balk at empty details
        }

        // first must have an actual fiscal year
        if (socChangeDetail.getChartOfAccounts() == null) {
            if (add) {
                errorPath = KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_EXIST);
            } else {
                errorPath = KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "[" + lineNum + "]." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_EXIST);
            }
            success &= false;
            return success;
        }

        // the two fiscal years from the document and detail must match
        if (!socChangeDocument.getChartOfAccounts().equals(socChangeDetail.getChartOfAccounts())) {
            if (add) {
                errorPath = KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_BE_SAME);
            } else {
                errorPath = KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "[" + lineNum + "]." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_BE_SAME);
            }
            success &= false;
            return success;
        }

        return success;
    }

    /**
     * This checks that the chart of accounts on the {@link AccountGlobalDetail} is not empty and matches
     * the document's chart matches the detail's chart
     *
     * @param socChangeDocument
     * @param acctDetail
     * @param lineNum
     * @param add
     * @return false if the chart is missing or is not the same between the doc and the detail
     */
    protected boolean checkChartOnAccountDetails(SubObjectCodeGlobal socChangeDocument, AccountGlobalDetail acctDetail, int lineNum, boolean add) {
        boolean success = true;
        String errorPath = KFSConstants.EMPTY_STRING;
        // first must have an actual fiscal year
        if (acctDetail.getChartOfAccounts() == null) {
            if (add) {
                errorPath = KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_EXIST);
            } else {
                errorPath = KFSPropertyConstants.SUB_OBJ_CODE_CHANGE_DETAILS + "[" + lineNum + "]." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_EXIST);
            }
            success &= false;
            return success;
        }

        // the two fiscal years from the document and detail must match
        if (!socChangeDocument.getChartOfAccounts().equals(acctDetail.getChartOfAccounts())) {
            if (add) {
                errorPath = KFSConstants.MAINTENANCE_ADD_PREFIX + KFSPropertyConstants.ACCOUNT_CHANGE_DETAILS + "." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_BE_SAME);
            } else {
                errorPath = KFSPropertyConstants.ACCOUNT_CHANGE_DETAILS + "[" + lineNum + "]." + "chartOfAccountsCode";
                putFieldError(errorPath, KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_SUBOBJECTMAINT_CHART_MUST_BE_SAME);
            }
            success &= false;
            return success;
        }

        return success;
    }


}
