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


import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.BalanceType;
import org.kuali.kfs.coa.businessobject.Chart;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.ObjectType;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.businessobject.SubObjectCode;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.ld.LaborKeyConstants;
import org.kuali.kfs.module.ld.batch.service.LaborAccountingCycleCachingService;
import org.kuali.kfs.module.ld.businessobject.LaborOriginEntry;
import org.kuali.kfs.module.ld.businessobject.LaborTransaction;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.MessageBuilder;
import org.kuali.kfs.sys.businessobject.SystemOptions;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.Collection;

/**
 * This class provides a set of utilities that can be used to validate a transaction in the field level.
 */
public class TransactionFieldValidator {
    private static LaborAccountingCycleCachingService accountingCycleCachingService;
    private static ConfigurationService kualiConfigurationService;

    /**
     * Checks if the given transaction contains valid university fiscal year
     *
     * @param transaction the given transaction
     * @return null if the university fiscal year is valid; otherwise, return error message
     */
    public static Message checkUniversityFiscalYear(LaborTransaction transaction) {

        Integer fiscalYear = transaction.getUniversityFiscalYear();
        if (fiscalYear == null) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_UNIV_FISCAL_YR_NOT_FOUND, Message.TYPE_FATAL);
        } else {
            SystemOptions option = getAccountingCycleCachingService().getSystemOptions(((LaborOriginEntry) transaction).getUniversityFiscalYear());
            if (ObjectUtils.isNull(option)) {
                return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_UNIV_FISCAL_YR_NOT_FOUND, fiscalYear.toString(), Message.TYPE_FATAL);
            }
        }
        return null;
    }

    /**
     * Checks if the given transaction contains valid char of accounts code
     *
     * @param transaction the given transaction
     * @return null if the char of accounts code is valid; otherwise, return error message
     */
    public static Message checkChartOfAccountsCode(LaborTransaction transaction) {
        String chartOfAccountsCode = transaction.getChartOfAccountsCode();
        Chart chart = getAccountingCycleCachingService().getChart(((LaborOriginEntry) transaction).getChartOfAccountsCode());
        if (StringUtils.isBlank(chartOfAccountsCode) || ObjectUtils.isNull(chart)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_CHART_NOT_FOUND, chartOfAccountsCode, Message.TYPE_FATAL);
        }

        if (!chart.isActive()) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_CHART_NOT_ACTIVE, chartOfAccountsCode, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains valid account number
     *
     * @param transaction the given transaction
     * @return null if the account number is valid; otherwise, return error message
     */
    public static Message checkAccountNumber(LaborTransaction transaction) {
        String accountNumber = transaction.getAccountNumber();
        Account account = getAccountingCycleCachingService().getAccount(((LaborOriginEntry) transaction).getChartOfAccountsCode(), ((LaborOriginEntry) transaction).getAccountNumber());
        if (StringUtils.isBlank(accountNumber) || ObjectUtils.isNull(account)) {
            String chartOfAccountsCode = transaction.getChartOfAccountsCode();
            String accountKey = chartOfAccountsCode + "-" + accountNumber;
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_ACCOUNT_NOT_FOUND, accountKey, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains valid sub account number
     *
     * @param transaction the given transaction
     * @return null if the sub account number is valid; otherwise, return error message
     */
    public static Message checkSubAccountNumber(LaborTransaction transaction) {
        return checkSubAccountNumber(transaction, null);
    }

    /**
     * Checks if the given transaction contains valid sub account number
     *
     * @param transaction               the given transaction
     * @param exclusiveDocumentTypeCode inactive sub account can be OK if the document type of the given transaction is
     *                                  exclusiveDocumentTypeCode
     * @return null if the sub account number is valid; otherwise, return error message
     */
    public static Message checkSubAccountNumber(LaborTransaction transaction, String exclusiveDocumentTypeCode) {
        String subAccountNumber = transaction.getSubAccountNumber();
        String chartOfAccountsCode = transaction.getChartOfAccountsCode();
        String accountNumber = transaction.getAccountNumber();
        String documentTypeCode = transaction.getFinancialDocumentTypeCode();
        String subAccountKey = chartOfAccountsCode + "-" + accountNumber + "-" + subAccountNumber;
        SubAccount subAccount = getAccountingCycleCachingService().getSubAccount(((LaborOriginEntry) transaction).getChartOfAccountsCode(), ((LaborOriginEntry) transaction).getAccountNumber(), ((LaborOriginEntry) transaction).getSubAccountNumber());

        if (StringUtils.isBlank(subAccountNumber)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_SUB_ACCOUNT_NOT_FOUND, subAccountKey, Message.TYPE_FATAL);
        }

        if (!KFSConstants.getDashSubAccountNumber().equals(subAccountNumber)) {
            if (ObjectUtils.isNull(subAccount)) {
                return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_SUB_ACCOUNT_NOT_FOUND, subAccountKey, Message.TYPE_FATAL);
            }

            if (!StringUtils.equals(documentTypeCode, exclusiveDocumentTypeCode)) {
                if (!subAccount.isActive()) {
                    return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_SUB_ACCOUNT_NOT_ACTIVE, subAccountKey, Message.TYPE_FATAL);
                }
            }
        }
        return null;
    }

    /**
     * Checks if the given transaction contains valid account number
     *
     * @param transaction the given transaction
     * @return null if the account number is valid; otherwise, return error message
     */
    public static Message checkFinancialObjectCode(LaborTransaction transaction) {
        String objectCode = transaction.getFinancialObjectCode();
        if (StringUtils.isBlank(objectCode)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_OBJECT_CODE_EMPTY, Message.TYPE_FATAL);
        }

        Integer fiscalYear = transaction.getUniversityFiscalYear();
        String chartOfAccountsCode = transaction.getChartOfAccountsCode();
        String objectCodeKey = fiscalYear + "-" + chartOfAccountsCode + "-" + objectCode;
        ObjectCode financialObject = getAccountingCycleCachingService().getObjectCode(((LaborOriginEntry) transaction).getUniversityFiscalYear(), ((LaborOriginEntry) transaction).getChartOfAccountsCode(), ((LaborOriginEntry) transaction).getFinancialObjectCode());

        //do we need it?
        transaction.refreshNonUpdateableReferences();

        if (ObjectUtils.isNull(financialObject)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_OBJECT_CODE_NOT_FOUND, objectCodeKey, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains valid sub object code
     *
     * @param transaction the given transaction
     * @return null if the sub object code is valid; otherwise, return error message
     */
    public static Message checkFinancialSubObjectCode(LaborTransaction transaction) {
        Integer fiscalYear = transaction.getUniversityFiscalYear();
        String chartOfAccountsCode = transaction.getChartOfAccountsCode();
        String objectCode = transaction.getFinancialObjectCode();
        String subObjectCode = transaction.getFinancialSubObjectCode();

        String subObjectCodeKey = fiscalYear + "-" + chartOfAccountsCode + "-" + objectCode + "-" + subObjectCode;
        if (StringUtils.isBlank(subObjectCode)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_SUB_OBJECT_CODE_NOT_BE_NULL, subObjectCodeKey, Message.TYPE_FATAL);
        }
        SubObjectCode financialSubObject = getAccountingCycleCachingService().getSubObjectCode(((LaborOriginEntry) transaction).getUniversityFiscalYear(), ((LaborOriginEntry) transaction).getChartOfAccountsCode(), ((LaborOriginEntry) transaction).getAccountNumber(), ((LaborOriginEntry) transaction).getFinancialObjectCode(), ((LaborOriginEntry) transaction).getFinancialSubObjectCode());
        if (!KFSConstants.getDashFinancialSubObjectCode().equals(subObjectCode)) {
            if (ObjectUtils.isNull(financialSubObject)) {
                return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_SUB_OBJECT_CODE_NOT_BE_NULL, subObjectCodeKey, Message.TYPE_FATAL);
            }
        }
        return null;
    }

    /**
     * Checks if the given transaction contains valid balance type code
     *
     * @param transaction the given transaction
     * @return null if the balance type code is valid; otherwise, return error message
     */
    public static Message checkFinancialBalanceTypeCode(LaborTransaction transaction) {
        String balanceTypeCode = transaction.getFinancialBalanceTypeCode();
        BalanceType balanceType = getAccountingCycleCachingService().getBalanceType(((LaborOriginEntry) transaction).getFinancialBalanceTypeCode());
        if (StringUtils.isBlank(balanceTypeCode) || ObjectUtils.isNull(balanceType)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_BALANCE_TYPE_NOT_FOUND, balanceTypeCode, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains valid object type code
     *
     * @param transaction the given transaction
     * @return null if the object type code is valid; otherwise, return error message
     */
    public static Message checkFinancialObjectTypeCode(LaborTransaction transaction) {
        String objectTypeCode = transaction.getFinancialObjectTypeCode();
        ObjectType objectType = getAccountingCycleCachingService().getObjectType(((LaborOriginEntry) transaction).getFinancialObjectTypeCode());
        if (StringUtils.isBlank(objectTypeCode) || ObjectUtils.isNull(objectType)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_OBJECT_TYPE_NOT_FOUND, objectTypeCode, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains university fiscal period code
     *
     * @param transaction the given transaction
     * @return null if the university fiscal period code is valid; otherwise, return error message
     */
    public static Message checkUniversityFiscalPeriodCode(LaborTransaction transaction) {
        String fiscalPeriodCode = transaction.getUniversityFiscalPeriodCode();
        if (StringUtils.isBlank(fiscalPeriodCode)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_ACCOUNTING_PERIOD_NOT_FOUND, fiscalPeriodCode, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains document type code
     *
     * @param transaction the given transaction
     * @return null if the document type code is valid; otherwise, return error message
     */
    public static Message checkFinancialDocumentTypeCode(LaborTransaction transaction) {
        if (StringUtils.isBlank(transaction.getFinancialDocumentTypeCode()) || !getAccountingCycleCachingService().isCurrentActiveAccountingDocumentType(transaction.getFinancialDocumentTypeCode())) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_DOCUMENT_TYPE_NOT_FOUND, transaction.getFinancialDocumentTypeCode(), Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains document number
     *
     * @param transaction the given transaction
     * @return null if the document number is valid; otherwise, return error message
     */
    public static Message checkFinancialDocumentNumber(LaborTransaction transaction) {
        String documentNumber = transaction.getDocumentNumber();
        if (StringUtils.isBlank(documentNumber)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_DOCUMENT_NUMBER_REQUIRED, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains transaction sequence number
     *
     * @param transaction the given transaction
     * @return null if the transaction sequence number is valid; otherwise, return error message
     */
    // Don't need to check SequenceNumber because it sets in each poster (LaborLedgerEntryPoster and LaborGLLedgerEntryPoster), so commented out
//    public static Message checkTransactionLedgerEntrySequenceNumber(LaborTransaction transaction) {
//        Integer sequenceNumber = transaction.getTransactionLedgerEntrySequenceNumber();
//        if (sequenceNumber == null) {
//            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_SEQUENCE_NUMBER_NOT_BE_NULL, Message.TYPE_FATAL);
//        }
//        return null;
//    }

    /**
     * Checks if the given transaction contains debit credit code
     *
     * @param transaction the given transaction
     * @return null if the debit credit code is valid; otherwise, return error message
     */
    public static Message checkTransactionDebitCreditCode(LaborTransaction transaction) {
        String[] validDebitCreditCode = {KFSConstants.GL_BUDGET_CODE, KFSConstants.GL_CREDIT_CODE, KFSConstants.GL_DEBIT_CODE};
        String debitCreditCode = transaction.getTransactionDebitCreditCode();
        if (debitCreditCode == null || !ArrayUtils.contains(validDebitCreditCode, debitCreditCode)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_DEDIT_CREDIT_CODE_NOT_BE_NULL, Message.TYPE_FATAL);
        } else if (transaction.getBalanceType().isFinancialOffsetGenerationIndicator() && !KFSConstants.GL_DEBIT_CODE.equals(transaction.getTransactionDebitCreditCode()) && !KFSConstants.GL_CREDIT_CODE.equals(transaction.getTransactionDebitCreditCode())) {
            return new Message(getConfigurationService().getPropertyValueAsString(KFSKeyConstants.MSG_DEDIT_CREDIT_CODE_MUST_BE) + " '" + KFSConstants.GL_DEBIT_CODE + " or " + KFSConstants.GL_CREDIT_CODE + getConfigurationService().getPropertyValueAsString(KFSKeyConstants.MSG_FOR_BALANCE_TYPE), Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains system origination code
     *
     * @param transaction the given transaction
     * @return null if the system origination code is valid; otherwise, return error message
     */
    public static Message checkFinancialSystemOriginationCode(LaborTransaction transaction) {
        String originationCode = transaction.getFinancialSystemOriginationCode();
        if (StringUtils.isBlank(originationCode)) {
            return MessageBuilder.buildMessage(KFSKeyConstants.ERROR_ORIGIN_CODE_NOT_FOUND, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains the posteable period code
     *
     * @param transaction          the given transaction
     * @param unpostableperidCodes the list of unpostable period code
     * @return null if the perid code of the transaction is not in unpostableperidCodes; otherwise, return error message
     */
    public static Message checkPostablePeridCode(LaborTransaction transaction, Collection<String> unpostableperidCodes) {
        String periodCode = transaction.getUniversityFiscalPeriodCode();
        if (unpostableperidCodes.contains(periodCode)) {
            return MessageBuilder.buildMessage(LaborKeyConstants.ERROR_UNPOSTABLE_PERIOD_CODE, periodCode, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains the posteable balance type code
     *
     * @param transaction                the given transaction
     * @param unpostableBalanceTypeCodes the list of unpostable balance type codes
     * @return null if the balance type code of the transaction is not in unpostableBalanceTypeCodes; otherwise, return error
     * message
     */
    public static Message checkPostableBalanceTypeCode(LaborTransaction transaction, Collection<String> unpostableBalanceTypeCodes) {
        String balanceTypeCode = transaction.getFinancialBalanceTypeCode();
        if (unpostableBalanceTypeCodes.contains(balanceTypeCode)) {
            return MessageBuilder.buildMessage(LaborKeyConstants.ERROR_UNPOSTABLE_BALANCE_TYPE, balanceTypeCode, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the transaction amount of the given transaction is ZERO
     *
     * @param transaction the given transaction
     * @return null if the transaction amount is not ZERO or null; otherwise, return error message
     */
    public static Message checkZeroTotalAmount(LaborTransaction transaction) {
        KualiDecimal amount = transaction.getTransactionLedgerEntryAmount();
        if (amount == null || amount.isZero()) {
            return MessageBuilder.buildMessage(LaborKeyConstants.ERROR_ZERO_TOTAL_AMOUNT, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * Checks if the given transaction contains the valid employee id
     *
     * @param transaction           the given transaction
     * @param unpostableObjectCodes the list of unpostable object codes
     * @return null if the object code of the transaction is not in unpostableObjectCodes; otherwise, return error message
     */
    public static Message checkEmplid(LaborTransaction transaction) {
        String emplid = transaction.getEmplid();
        if (StringUtils.isBlank(emplid)) {
            return MessageBuilder.buildMessage(LaborKeyConstants.MISSING_EMPLOYEE_ID, Message.TYPE_FATAL);
        }
        return null;
    }

    /**
     * When in Rome... This method checks if the encumbrance update code is valid
     *
     * @param transaction the transaction to check
     * @return a Message if the encumbrance update code is not valid, or null if all is well
     */
    public static Message checkEncumbranceUpdateCode(LaborTransaction transaction) {
        // The encumbrance update code can only be space, N, R or D. Nothing else
        if ((StringUtils.isNotBlank(transaction.getTransactionEncumbranceUpdateCode())) && (!" ".equals(transaction.getTransactionEncumbranceUpdateCode())) && (!KFSConstants.ENCUMB_UPDT_NO_ENCUMBRANCE_CD.equals(transaction.getTransactionEncumbranceUpdateCode())) && (!KFSConstants.ENCUMB_UPDT_REFERENCE_DOCUMENT_CD.equals(transaction.getTransactionEncumbranceUpdateCode())) && (!KFSConstants.ENCUMB_UPDT_DOCUMENT_CD.equals(transaction.getTransactionEncumbranceUpdateCode()))) {
            return new Message("Invalid Encumbrance Update Code (" + transaction.getTransactionEncumbranceUpdateCode() + ")", Message.TYPE_FATAL);
        }
        return null;
    }

    static LaborAccountingCycleCachingService getAccountingCycleCachingService() {
        if (accountingCycleCachingService == null) {
            accountingCycleCachingService = SpringContext.getBean(LaborAccountingCycleCachingService.class);
        }
        return accountingCycleCachingService;
    }

    static ConfigurationService getConfigurationService() {
        if (kualiConfigurationService == null) {
            kualiConfigurationService = SpringContext.getBean(ConfigurationService.class);
        }
        return kualiConfigurationService;
    }
}
