/*
 * 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.gl.batch;

import org.apache.log4j.Logger;
import org.kuali.kfs.coa.service.ObjectTypeService;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.ObjectHelper;
import org.kuali.kfs.gl.batch.service.impl.exception.FatalErrorException;
import org.kuali.kfs.gl.businessobject.Balance;
import org.kuali.kfs.gl.businessobject.OriginEntryFull;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.FlexibleOffsetAccountService;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;

import java.sql.Date;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * This class helps generate the entries for the nominal activity closing year end job.
 */
public class NominalActivityClosingHelper {
    private Integer fiscalYear;
    private Date transactionDate;
    private String varNetExpenseObjectCode;
    private String varNetRevenueObjectCode;
    private String varFundBalanceObjectCode;
    private String varFundBalanceObjectTypeCode;
    private String[] expenseObjectCodeTypes;
    private ParameterService parameterService;
    private ConfigurationService configurationService;
    private FlexibleOffsetAccountService flexibleOffsetService;
    private Logger LOG = Logger.getLogger(NominalActivityClosingHelper.class);
    private int nonFatalErrorCount;
    private List<String> varCharts;

    /**
     * Constructs a NominalActivityClosingHelper
     *
     * @param fiscalYear           the fiscal year this job is being run for
     * @param transactionDate      the transaction date that origin entries should hit the ledger
     * @param parameterService     an implementation of the ParameterService
     * @param configurationService an implementation of the ConfigurationService
     */
    public NominalActivityClosingHelper(Integer fiscalYear, Date transactionDate, ParameterService parameterService, ConfigurationService configurationService, ObjectTypeService objectTypeService) {
        this.fiscalYear = fiscalYear;
        this.transactionDate = transactionDate;
        this.parameterService = parameterService;
        this.configurationService = configurationService;
        this.flexibleOffsetService = SpringContext.getBean(FlexibleOffsetAccountService.class);
        this.nonFatalErrorCount = 0;

        List<String> objectTypes = objectTypeService.getExpenseObjectTypes(fiscalYear);
        expenseObjectCodeTypes = objectTypes.toArray(new String[0]);

        varNetExpenseObjectCode = parameterService.getParameterValueAsString(NominalActivityClosingStep.class, "NET_EXPENSE_OBJECT_CODE");
        varNetRevenueObjectCode = parameterService.getParameterValueAsString(NominalActivityClosingStep.class, "NET_REVENUE_OBJECT_CODE");
        varFundBalanceObjectCode = parameterService.getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, GeneralLedgerConstants.ANNUAL_CLOSING_FUND_BALANCE_OBJECT_CODE_PARM);
        varFundBalanceObjectTypeCode = parameterService.getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, GeneralLedgerConstants.ANNUAL_CLOSING_FUND_BALANCE_OBJECT_TYPE_PARM);

        //Obtain list of charts to close from Parameter ANNUAL_CLOSING_CHARTS_PARAM.
        //If no parameter value exists, act on all charts which is the default action in the delivered foundation code.
        varCharts = new ArrayList<String>();

        Collection<String> annualClosingCharts = parameterService.getParameterValuesAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, GeneralLedgerConstants.ANNUAL_CLOSING_CHARTS_PARAM);

        if (ObjectUtils.isNotNull(annualClosingCharts) && (!annualClosingCharts.isEmpty())) {
            //transfer charts from parameter to List
            varCharts.addAll(annualClosingCharts);

            LOG.info("NominalActivityClosingJob ANNUAL_CLOSING_CHARTS parameter value = " + varCharts.toString());
        }

    }

    /**
     * Generates an origin entry that will summarize close out of nominal items (income and expense)
     *
     * @param balance        the balance this activity closing entry needs to be created for
     * @param sequenceNumber the sequence number of the origin entry
     * @return an origin entry which will close out nominal activity on a balance
     * @throws FatalErrorException thrown if the given balance lacks an object type code
     */
    public OriginEntryFull generateActivityEntry(Balance balance, Integer sequenceNumber) throws FatalErrorException {

        OriginEntryFull activityEntry = new OriginEntryFull();

        activityEntry.setUniversityFiscalYear(fiscalYear);
        activityEntry.setChartOfAccountsCode(balance.getChartOfAccountsCode());
        activityEntry.setAccountNumber(balance.getAccountNumber());
        activityEntry.setSubAccountNumber(balance.getSubAccountNumber());

        if (ObjectHelper.isOneOf(balance.getObjectTypeCode(), expenseObjectCodeTypes)) {
            activityEntry.setFinancialObjectCode(varNetExpenseObjectCode);
        } else {
            activityEntry.setFinancialObjectCode(varNetRevenueObjectCode);
        }

        activityEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        activityEntry.setFinancialBalanceTypeCode(balance.getOption().getNominalFinancialBalanceTypeCd());

        if (null == balance.getObjectTypeCode()) {
            throw new FatalErrorException(" BALANCE SELECTED FOR PROCESSING IS MISSING ITS OBJECT TYPE CODE ");

        }

        activityEntry.setFinancialObjectTypeCode(balance.getObjectTypeCode());
        activityEntry.setUniversityFiscalPeriodCode(KFSConstants.MONTH13);
        activityEntry.setFinancialDocumentTypeCode(parameterService.getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, KFSConstants.SystemGroupParameterNames.GL_ANNUAL_CLOSING_DOC_TYPE));
        activityEntry.setFinancialSystemOriginationCode(parameterService.getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, KFSConstants.SystemGroupParameterNames.GL_ORIGINATION_CODE));
        activityEntry.setDocumentNumber(new StringBuffer(balance.getOption().getActualFinancialBalanceTypeCd()).append(balance.getAccountNumber()).toString());
        activityEntry.setTransactionLedgerEntrySequenceNumber(sequenceNumber);

        if (ObjectHelper.isOneOf(balance.getObjectTypeCode(), expenseObjectCodeTypes)) {
            activityEntry.setTransactionLedgerEntryDescription(this.createTransactionLedgerEntryDescription(configurationService.getPropertyValueAsString(KFSKeyConstants.MSG_CLOSE_ENTRY_TO_NOMINAL_EXPENSE), balance));
        } else {
            activityEntry.setTransactionLedgerEntryDescription(this.createTransactionLedgerEntryDescription(configurationService.getPropertyValueAsString(KFSKeyConstants.MSG_CLOSE_ENTRY_TO_NOMINAL_REVENUE), balance));
        }

        activityEntry.setTransactionLedgerEntryAmount(balance.getAccountLineAnnualBalanceAmount());
        activityEntry.setFinancialObjectTypeCode(balance.getObjectTypeCode());

        String debitCreditCode = null;

        if (null != balance.getObjectType()) {

            if (ObjectHelper.isOneOf(balance.getObjectType().getFinObjectTypeDebitcreditCd(), new String[]{KFSConstants.GL_CREDIT_CODE, KFSConstants.GL_DEBIT_CODE})) {
                debitCreditCode = balance.getObjectType().getFinObjectTypeDebitcreditCd();
            } else {
                debitCreditCode = KFSConstants.GL_CREDIT_CODE;
            }

            // NOTE (laran) The amount on the origin entry is set to this value above.
            // NOTE (laran) I'm using the balance value here because to me it was easier than remembering the
            // indirection.
            if (balance.getAccountLineAnnualBalanceAmount().isNegative()) {

                if (ObjectHelper.isOneOf(balance.getObjectType().getFinObjectTypeDebitcreditCd(), new String[]{KFSConstants.GL_CREDIT_CODE, KFSConstants.GL_DEBIT_CODE})) {
                    activityEntry.setTransactionDebitCreditCode(balance.getObjectType().getFinObjectTypeDebitcreditCd());
                } else {
                    activityEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
                }

            } else {
                if (KFSConstants.GL_CREDIT_CODE.equals(balance.getObjectType().getFinObjectTypeDebitcreditCd())) {
                    activityEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
                } else {
                    if (KFSConstants.GL_DEBIT_CODE.equals(balance.getObjectType().getFinObjectTypeDebitcreditCd())) {
                        activityEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
                    } else {
                        activityEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
                    }
                }
            }
        } else {
            if (balance.getAccountLineAnnualBalanceAmount().isNegative()) {
                activityEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            } else {
                activityEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            }

            nonFatalErrorCount += 1;

            LOG.info(new StringBuffer("FIN OBJ TYP CODE ").append(balance.getObjectTypeCode()).append(" NOT IN TABLE").toString());

        }

        activityEntry.setTransactionDate(transactionDate);
        activityEntry.setOrganizationDocumentNumber(null);
        activityEntry.setProjectCode(KFSConstants.getDashProjectCode());
        activityEntry.setOrganizationReferenceId(null);
        activityEntry.setReferenceFinancialDocumentTypeCode(null);
        activityEntry.setReferenceFinancialSystemOriginationCode(null);
        activityEntry.setReferenceFinancialDocumentNumber(null);
        activityEntry.setReversalDate(null);
        activityEntry.setTransactionEncumbranceUpdateCode(null);

        if (balance.getAccountLineAnnualBalanceAmount().isNegative()) {
            activityEntry.setTransactionLedgerEntryAmount(balance.getAccountLineAnnualBalanceAmount().negated());
        }

        return activityEntry;

    }

    /**
     * Genereates an origin entry to update a fund balance as a result of closing income and expense
     *
     * @param balance        the balance this offset needs to be created for
     * @param sequenceNumber the sequence number of the origin entry full
     * @return an origin entry which will offset the nominal closing activity
     * @throws FatalErrorException thrown if the given balance lacks an object type code
     */
    public OriginEntryFull generateOffset(Balance balance, Integer sequenceNumber) throws FatalErrorException {
        String debitCreditCode = null;

        if (null == balance.getObjectTypeCode()) {
            throw new FatalErrorException(" BALANCE SELECTED FOR PROCESSING IS MISSING ITS OBJECT TYPE CODE ");

        }

        if (ObjectHelper.isOneOf(balance.getObjectType().getFinObjectTypeDebitcreditCd(), new String[]{KFSConstants.GL_CREDIT_CODE, KFSConstants.GL_DEBIT_CODE})) {
            debitCreditCode = balance.getObjectType().getFinObjectTypeDebitcreditCd();
        } else {
            debitCreditCode = KFSConstants.GL_CREDIT_CODE;
        }

        OriginEntryFull offsetEntry = new OriginEntryFull();
        offsetEntry.setUniversityFiscalYear(fiscalYear);
        offsetEntry.setChartOfAccountsCode(balance.getChartOfAccountsCode());
        offsetEntry.setAccountNumber(balance.getAccountNumber());
        offsetEntry.setSubAccountNumber(balance.getSubAccountNumber());
        offsetEntry.setFinancialObjectCode(varFundBalanceObjectCode);
        offsetEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        offsetEntry.setFinancialBalanceTypeCode(balance.getOption().getNominalFinancialBalanceTypeCd());
        offsetEntry.setFinancialObjectTypeCode(varFundBalanceObjectTypeCode);
        offsetEntry.setUniversityFiscalPeriodCode(KFSConstants.MONTH13);
        offsetEntry.setFinancialDocumentTypeCode(parameterService.getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, KFSConstants.SystemGroupParameterNames.GL_ANNUAL_CLOSING_DOC_TYPE));
        offsetEntry.setFinancialSystemOriginationCode(parameterService.getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, KFSConstants.SystemGroupParameterNames.GL_ORIGINATION_CODE));
        offsetEntry.setDocumentNumber(new StringBuffer(balance.getOption().getActualFinancialBalanceTypeCd()).append(balance.getAccountNumber()).toString());
        offsetEntry.setTransactionLedgerEntrySequenceNumber(new Integer(sequenceNumber.intValue()));
        offsetEntry.setTransactionLedgerEntryDescription(this.createTransactionLedgerEntryDescription(configurationService.getPropertyValueAsString(KFSKeyConstants.MSG_CLOSE_ENTRY_TO_FUND_BALANCE), balance));
        offsetEntry.setTransactionLedgerEntryAmount(balance.getAccountLineAnnualBalanceAmount());
        offsetEntry.setTransactionDebitCreditCode(debitCreditCode);
        offsetEntry.setTransactionDate(transactionDate);
        offsetEntry.setOrganizationDocumentNumber(null);
        offsetEntry.setProjectCode(KFSConstants.getDashProjectCode());
        offsetEntry.setOrganizationReferenceId(null);
        offsetEntry.setReferenceFinancialDocumentTypeCode(null);
        offsetEntry.setReferenceFinancialSystemOriginationCode(null);
        offsetEntry.setReferenceFinancialDocumentNumber(null);
        offsetEntry.setFinancialDocumentReversalDate(null);
        offsetEntry.setTransactionEncumbranceUpdateCode(null);

        if (balance.getAccountLineAnnualBalanceAmount().isNegative()) {
            if (KFSConstants.GL_CREDIT_CODE.equals(debitCreditCode)) {
                offsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            } else {
                offsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            }

        }

        if (balance.getAccountLineAnnualBalanceAmount().isNegative()) {
            offsetEntry.setTransactionLedgerEntryAmount(balance.getAccountLineAnnualBalanceAmount().negated());
        }

        flexibleOffsetService.updateOffset(offsetEntry);

        return offsetEntry;
    }

    /**
     * Adds the job parameters used to generate the origin entries to the given map
     *
     * @param nominalClosingJobParameters a map of batch job parameters to add nominal activity closing parameters to
     */
    public void addNominalClosingJobParameters(Map nominalClosingJobParameters) {
        nominalClosingJobParameters.put(GeneralLedgerConstants.ColumnNames.UNIVERSITY_FISCAL_YEAR, fiscalYear);
        nominalClosingJobParameters.put(GeneralLedgerConstants.ColumnNames.NET_EXP_OBJECT_CD, varNetExpenseObjectCode);
        nominalClosingJobParameters.put(GeneralLedgerConstants.ColumnNames.NET_REV_OBJECT_CD, varNetRevenueObjectCode);
        nominalClosingJobParameters.put(GeneralLedgerConstants.ColumnNames.FUND_BAL_OBJECT_CD, varFundBalanceObjectCode);
        nominalClosingJobParameters.put(GeneralLedgerConstants.ColumnNames.FUND_BAL_OBJ_TYP_CD, varFundBalanceObjectTypeCode);
        nominalClosingJobParameters.put(GeneralLedgerConstants.ColumnNames.CHART_OF_ACCOUNTS_CODE, varCharts);
    }

    /**
     * Generates the transaction ledger entry description for a given balance
     *
     * @param descriptorIntro the introduction to the description
     * @param balance         the balance the transaction description will refer to
     * @return the generated transaction ledger entry description
     */
    private String createTransactionLedgerEntryDescription(String descriptorIntro, Balance balance) {
        StringBuilder description = new StringBuilder();
        description.append(descriptorIntro.trim()).append(' ');
        return description.append(getSizedField(5, balance.getSubAccountNumber())).append("-").append(getSizedField(4, balance.getObjectCode())).append("-").append(getSizedField(3, balance.getSubObjectCode())).append("-").append(getSizedField(2, balance.getObjectTypeCode())).toString();
    }

    /**
     * Pads out a string so that it will be a certain length
     *
     * @param size  the size to pad to
     * @param value the String being padded
     * @return the padded String
     */
    private StringBuilder getSizedField(int size, String value) {
        StringBuilder fieldString = new StringBuilder();
        if (value != null) {
            fieldString.append(value);
            while (fieldString.length() < size) {
                fieldString.append(' ');
            }
        } else {
            while (fieldString.length() < size) {
                fieldString.append('-');
            }
        }
        return fieldString;
    }

    /**
     * Returns the count of non-fatal errors encountered during the process by this helper
     *
     * @return the count of non-fatal errors
     */
    public Integer getNonFatalErrorCount() {
        return new Integer(this.nonFatalErrorCount);
    }

    /**
     * Returns the boolean from the chart parameter list being empty
     *
     * @return isEmpty boolean value for chart List
     */
    public boolean isAnnualClosingChartParamterBlank() {
        return varCharts.isEmpty();
    }

}
