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

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
import org.kuali.kfs.gl.report.PosterOutputSummaryReport;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.module.ld.LaborConstants;
import org.kuali.kfs.module.ld.LaborConstants.YearEnd;
import org.kuali.kfs.module.ld.LaborKeyConstants;
import org.kuali.kfs.module.ld.batch.LaborYearEndBalanceForwardStep;
import org.kuali.kfs.module.ld.batch.service.LaborYearEndBalanceForwardService;
import org.kuali.kfs.module.ld.businessobject.LaborOriginEntry;
import org.kuali.kfs.module.ld.businessobject.LedgerBalanceForYearEndBalanceForward;
import org.kuali.kfs.module.ld.service.LaborLedgerBalanceService;
import org.kuali.kfs.module.ld.util.DebitCreditUtil;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.ObjectUtil;
import org.kuali.kfs.sys.businessobject.SystemOptions;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.OptionsService;
import org.kuali.kfs.sys.service.ReportWriterService;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static org.kuali.kfs.module.ld.LaborConstants.DestinationNames.LEDGER_BALANCE;
import static org.kuali.kfs.module.ld.LaborConstants.DestinationNames.ORIGN_ENTRY;

/**
 * Labor Ledger Year End Inception to Date Beginning Balance process moves the Year-to-Date Total plus the Contracts and Grants
 * Beginning Balances to the Contracts and Grants Beginning Balances of the new fiscal year for a designated group of accounts (to
 * be identified by fund group and sub fund group).
 */
@Transactional
public class LaborYearEndBalanceForwardServiceImpl implements LaborYearEndBalanceForwardService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LaborYearEndBalanceForwardServiceImpl.class);

    private LaborLedgerBalanceService laborLedgerBalanceService;
    private OptionsService optionsService;

    private BusinessObjectService businessObjectService;
    private DateTimeService dateTimeService;
    private ParameterService parameterService;
    private String batchFileDirectoryName;

    private ReportWriterService laborBalanceForwardReportWriterService;

    protected static String PROCESSED_BALANCE_TYPES_LABEL = "PROCESSED BALANCE TYPES";
    protected static String PROCESSED_OBJECT_TYPES_LABEL = "PROCESSED OBJECT TYPES";

    /**
     * @see org.kuali.kfs.module.ld.batch.service.LaborYearEndBalanceForwardService#forwardBalance()
     */
    public void forwardBalance() {
        Integer fiscalYear = Integer.valueOf(parameterService.getParameterValueAsString(LaborYearEndBalanceForwardStep.class, YearEnd.OLD_FISCAL_YEAR));
        this.forwardBalance(fiscalYear);
    }

    /**
     * @see org.kuali.kfs.module.ld.batch.service.LaborYearEndBalanceForwardService#forwardBalance(java.lang.Integer)
     */
    public void forwardBalance(Integer fiscalYear) {
        forwardBalance(fiscalYear, fiscalYear + 1);
    }

    /**
     * @see org.kuali.kfs.module.ld.batch.service.LaborYearEndBalanceForwardService#forwardBalance(java.lang.Integer,
     * java.lang.Integer)
     */
    public void forwardBalance(Integer fiscalYear, Integer newFiscalYear) {
        SystemOptions options = optionsService.getOptions(fiscalYear);
        Date runDate = dateTimeService.getCurrentSqlDate();

        Map<String, Integer> reportSummary = this.constructReportSummary();
        PosterOutputSummaryReport posterOutputSummaryReport = new PosterOutputSummaryReport();

        Collection<String> processableBalanceTypeCodes = this.getProcessableBalanceTypeCode(options);
        Collection<String> processableObjectTypeCodes = this.getProcessableObjectTypeCodes(options);
        Collection<String> subFundGroupCodes = this.getSubFundGroupProcessed();
        Collection<String> fundGroupCodes = this.getFundGroupProcessed();

        // create files
        String balanceForwardsFileName = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.BALANCE_FORWARDS_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        File balanceForwardsFile = new File(balanceForwardsFileName);
        PrintStream balanceForwardsPs = null;

        try {
            balanceForwardsPs = new PrintStream(balanceForwardsFile);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("balanceForwardsFile Files doesn't exist " + balanceForwardsFileName);
        }

        // process the selected balances by balance type and object type
        Map<String, String> fieldValues = new HashMap<String, String>();
        int numberOfSelectedBalance = 0;
        for (String balanceTypeCode : processableBalanceTypeCodes) {
            fieldValues.put(KFSPropertyConstants.FINANCIAL_BALANCE_TYPE_CODE, balanceTypeCode);

            for (String objectTypeCode : processableObjectTypeCodes) {
                fieldValues.put(KFSPropertyConstants.FINANCIAL_OBJECT_TYPE_CODE, objectTypeCode);

                fieldValues.remove(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE);
                fieldValues.remove(KFSPropertyConstants.ACCOUNT_NUMBER);

                Iterator<LedgerBalanceForYearEndBalanceForward> balanceIterator = laborLedgerBalanceService.findBalancesForFiscalYear(fiscalYear, fieldValues, subFundGroupCodes, fundGroupCodes);
                numberOfSelectedBalance += postSelectedBalancesAsOriginEntries(balanceIterator, newFiscalYear, balanceForwardsPs, runDate, posterOutputSummaryReport, reportSummary);
            }
        }

        this.fillStatisticsReportWriter(reportSummary);
        fillParametersReportWriter(runDate, fiscalYear, fundGroupCodes, subFundGroupCodes, getOriginationCode(), processableBalanceTypeCodes, processableObjectTypeCodes, getDocumentTypeCode());
        posterOutputSummaryReport.writeReport(laborBalanceForwardReportWriterService);

        balanceForwardsPs.close();
        this.createDoneFile(balanceForwardsFileName);
    }

    // create a done file after balance forward completes.
    protected void createDoneFile(String originEntryFileName) {
        String doneFileName = originEntryFileName.replace(GeneralLedgerConstants.BatchFileSystem.EXTENSION, GeneralLedgerConstants.BatchFileSystem.DONE_FILE_EXTENSION);
        File doneFile = new File(doneFileName);

        if (!doneFile.exists()) {
            try {
                doneFile.createNewFile();
            } catch (IOException e) {
                throw new RuntimeException();
            }
        }
    }

    /**
     * post the qualified balances into origin entry table for the further labor ledger processing
     *
     * @param balanceIterator the given ledger balances that will be carried forward
     * @param newFiscalYear   the new fiscal year
     * @param validGroup      the group that the posted transaction belongs to
     * @param errorMap        the map that records the error messages
     * @param runDate         the date the transaction is posted
     * @return the number of qualified balances
     */
    protected int postSelectedBalancesAsOriginEntries(Iterator<LedgerBalanceForYearEndBalanceForward> balanceIterator, Integer newFiscalYear, PrintStream balanceForwardsPs, Date runDate, PosterOutputSummaryReport posterOutputSummaryReport, Map<String, Integer> reportSummary) {
        int numberOfSelectedBalance = 0;
        String description = this.getDescription();
        String originationCode = this.getOriginationCode();
        String documentTypeCode = this.getDocumentTypeCode();

        while (balanceIterator != null && balanceIterator.hasNext()) {
            LedgerBalanceForYearEndBalanceForward balance = balanceIterator.next();
            this.updateReportSummary(reportSummary, LEDGER_BALANCE, KFSConstants.OperationType.READ);

            List<Message> errors = null;

            boolean isValidBalance = validateBalance(balance, errors);
            LaborOriginEntry laborOriginEntry = new LaborOriginEntry();
            if (isValidBalance) {
                laborOriginEntry.setUniversityFiscalYear(newFiscalYear);
                laborOriginEntry.setFinancialDocumentTypeCode(documentTypeCode);
                laborOriginEntry.setFinancialSystemOriginationCode(originationCode);
                laborOriginEntry.setTransactionLedgerEntryDescription(description);

                this.postAsOriginEntry(balance, laborOriginEntry, balanceForwardsPs, runDate);
                numberOfSelectedBalance++;

                posterOutputSummaryReport.summarize((OriginEntryInformation) laborOriginEntry);
                this.updateReportSummary(reportSummary, LEDGER_BALANCE, KFSConstants.OperationType.SELECT);
                this.updateReportSummary(reportSummary, LEDGER_BALANCE, KFSConstants.OperationType.INSERT);
            } else if (errors != null && !errors.isEmpty()) {
                ObjectUtil.buildObject(laborOriginEntry, balance);

                laborBalanceForwardReportWriterService.writeError(laborOriginEntry, errors);
                this.updateReportSummary(reportSummary, LEDGER_BALANCE, KFSConstants.OperationType.REPORT_ERROR);
            }
        }

        return numberOfSelectedBalance;
    }

    /**
     * determine if the given balance is qualified to be carried forward to new fiscal year
     *
     * @param balance the given ledger balance that could be carried forward
     * @param errors  the error list that is updated if the given balacne is not qualified for carry forward
     * @return true if the balance is qualified; otherwise, false
     */
    protected boolean validateBalance(LedgerBalanceForYearEndBalanceForward balance, List<Message> errors) {
        /** This is the placeholder for addtional business rule validation. The former rules were moved down to data access layer. * */
        return true;
    }

    /**
     * post the qualified balance into origin entry table for the further labor ledger processing
     *
     * @param balance       the given ledger balance that will be carried forward
     * @param newFiscalYear the new fiscal year
     * @param validGroup    the group that the posted transaction belongs to
     * @param postingDate   the date the transaction is posted
     */
    protected void postAsOriginEntry(LedgerBalanceForYearEndBalanceForward balance, LaborOriginEntry originEntry, PrintStream balanceForwardsPs, Date postingDate) {
        try {
            originEntry.setAccountNumber(balance.getAccountNumber());
            originEntry.setChartOfAccountsCode(balance.getChartOfAccountsCode());
            originEntry.setSubAccountNumber(balance.getSubAccountNumber());
            originEntry.setFinancialObjectCode(balance.getFinancialObjectCode());
            originEntry.setFinancialSubObjectCode(balance.getFinancialSubObjectCode());
            originEntry.setFinancialBalanceTypeCode(balance.getFinancialBalanceTypeCode());
            originEntry.setFinancialObjectTypeCode(balance.getFinancialObjectTypeCode());

            originEntry.setPositionNumber(balance.getPositionNumber());
            originEntry.setEmplid(balance.getEmplid());
            originEntry.setDocumentNumber(balance.getFinancialBalanceTypeCode() + balance.getAccountNumber());

            originEntry.setProjectCode(KFSConstants.getDashProjectCode());
            originEntry.setUniversityFiscalPeriodCode(KFSConstants.PERIOD_CODE_CG_BEGINNING_BALANCE);

            KualiDecimal transactionAmount = balance.getAccountLineAnnualBalanceAmount();
            transactionAmount = transactionAmount.add(balance.getContractsGrantsBeginningBalanceAmount());

            originEntry.setTransactionLedgerEntryAmount(transactionAmount.abs());
            originEntry.setTransactionDebitCreditCode(DebitCreditUtil.getDebitCreditCode(transactionAmount, false));

            originEntry.setTransactionLedgerEntrySequenceNumber(null);
            originEntry.setTransactionTotalHours(BigDecimal.ZERO);
            originEntry.setTransactionDate(postingDate);

            try {
                balanceForwardsPs.printf("%s\n", originEntry.getLine());
            } catch (Exception e) {
                throw new RuntimeException(e.toString());
            }
        } catch (Exception e) {
            LOG.error(e);
        }
    }

    /**
     * get the fund group codes that are acceptable by year-end process
     *
     * @return the fund group codes that are acceptable by year-end process
     */
    protected Collection<String> getFundGroupProcessed() {
        return parameterService.getParameterValuesAsString(LaborYearEndBalanceForwardStep.class, YearEnd.FUND_GROUP_PROCESSED);
    }

    /**
     * get the fund group codes that are acceptable by year-end process
     *
     * @return the fund group codes that are acceptable by year-end process
     */
    protected Collection<String> getSubFundGroupProcessed() {
        return parameterService.getParameterValuesAsString(LaborYearEndBalanceForwardStep.class, YearEnd.SUB_FUND_GROUP_PROCESSED);
    }

    /**
     * get the balance type codes that are acceptable by year-end process
     *
     * @return the balance type codes that are acceptable by year-end process
     */
    protected List<String> getProcessableBalanceTypeCode(SystemOptions options) {
        List<String> processableBalanceTypeCodes = new ArrayList<String>();
        processableBalanceTypeCodes.add(options.getActualFinancialBalanceTypeCd());
        return processableBalanceTypeCodes;
    }

    /**
     * get the object type codes that are acceptable by year-end process
     *
     * @param options the given system options
     * @return the object type codes that are acceptable by year-end process
     */
    protected List<String> getProcessableObjectTypeCodes(SystemOptions options) {
        List<String> processableObjectTypeCodes = new ArrayList<String>();

        processableObjectTypeCodes.add(options.getFinObjTypeExpenditureexpCd());
        processableObjectTypeCodes.add(options.getFinObjTypeExpNotExpendCode());

        return processableObjectTypeCodes;
    }

    // fill the report writer with the collected data
    protected Map<String, Integer> constructReportSummary() {
        Map<String, Integer> reportSummary = new HashMap<String, Integer>();
        reportSummary.put(LEDGER_BALANCE + "," + KFSConstants.OperationType.READ, 0);
        reportSummary.put(LEDGER_BALANCE + "," + KFSConstants.OperationType.SELECT, 0);
        reportSummary.put(LEDGER_BALANCE + "," + KFSConstants.OperationType.REPORT_ERROR, 0);
        reportSummary.put(ORIGN_ENTRY + "," + KFSConstants.OperationType.INSERT, 0);

        return reportSummary;
    }

    // fill the gl entry report writer with the collected data
    protected void fillStatisticsReportWriter(Map<String, Integer> glEntryReportSummary) {
        laborBalanceForwardReportWriterService.writeStatisticLine("NUMBER OF RECORDS READ              %,9d", glEntryReportSummary.get(LEDGER_BALANCE + "," + KFSConstants.OperationType.READ));
        laborBalanceForwardReportWriterService.writeStatisticLine("NUMBER OF RECORDS SELECTED          %,9d", glEntryReportSummary.get(LEDGER_BALANCE + "," + KFSConstants.OperationType.SELECT));
        laborBalanceForwardReportWriterService.writeStatisticLine("NUMBER OF RECORDS IN ERROR          %,9d", glEntryReportSummary.get(LEDGER_BALANCE + "," + KFSConstants.OperationType.REPORT_ERROR));
        laborBalanceForwardReportWriterService.writeStatisticLine("NUMBER OF RECORDS INSERTED          %,9d", glEntryReportSummary.get(ORIGN_ENTRY + "," + KFSConstants.OperationType.INSERT));
    }

    /**
     * Report out which significant parameters were used in this report.
     *
     * @param runDate                   the date when this job was run
     * @param closingYear               the fiscal year closed by this job
     * @param processedFundGroups       the fund groups processed by this job
     * @param processedSubFundGroups    the sub-fund groups processed by this job
     * @param originationCode           the origination code of the posted entries
     * @param processedBalanceTypeCodes the balance type codes processed by this job
     * @param processedObjectTypeCodes  the object type codes processed by this job
     * @param documentTypeCode          the document type code of posted entries
     */
    protected void fillParametersReportWriter(Date runDate, Integer closingYear, Collection<String> processedFundGroups, Collection<String> processedSubFundGroups, String originationCode, Collection<String> processedBalanceTypeCodes, Collection<String> processedObjectTypeCodes, String documentTypeCode) {
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", GeneralLedgerConstants.ANNUAL_CLOSING_TRANSACTION_DATE_PARM, runDate.toString());
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", LaborConstants.YearEnd.OLD_FISCAL_YEAR, closingYear);
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", LaborConstants.YearEnd.FUND_GROUP_PROCESSED, StringUtils.join(processedFundGroups, ", "));
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", LaborConstants.YearEnd.SUB_FUND_GROUP_PROCESSED, StringUtils.join(processedSubFundGroups, ", "));
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", LaborConstants.YearEnd.ORIGINATION_CODE, originationCode);
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", LaborYearEndBalanceForwardServiceImpl.PROCESSED_BALANCE_TYPES_LABEL, StringUtils.join(processedBalanceTypeCodes, ", "));
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", LaborYearEndBalanceForwardServiceImpl.PROCESSED_OBJECT_TYPES_LABEL, StringUtils.join(processedObjectTypeCodes, ", "));
        laborBalanceForwardReportWriterService.writeParameterLine("%32s %10s", GeneralLedgerConstants.ANNUAL_CLOSING_DOCUMENT_TYPE, documentTypeCode);
        laborBalanceForwardReportWriterService.pageBreak();
    }

    // update the entry in the given report summary
    protected void updateReportSummary(Map<String, Integer> reportSummary, String destination, String operation) {
        String key = destination + "," + operation;

        if (reportSummary.containsKey(key)) {
            Integer count = reportSummary.get(key);
            reportSummary.put(key, count + 1);
        } else {
            reportSummary.put(key, 1);
        }
    }

    /**
     * get the document type code of the transaction posted by year-end process
     *
     * @return the document type code of the transaction posted by year-end process
     */
    protected String getDocumentTypeCode() {
        return parameterService.getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, GeneralLedgerConstants.ANNUAL_CLOSING_DOCUMENT_TYPE);
    }

    /**
     * get the origination code of the transaction posted by year-end process
     *
     * @return the origination code of the transaction posted by year-end process
     */
    protected String getOriginationCode() {
        return parameterService.getParameterValueAsString(LaborYearEndBalanceForwardStep.class, YearEnd.ORIGINATION_CODE);
    }

    /**
     * get the description of the transaction posted by year-end process
     *
     * @return the description of the transaction posted by year-end process
     */
    protected String getDescription() {
        return SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(LaborKeyConstants.MESSAGE_YEAR_END_TRANSACTION_DESCRIPTON);
    }

    /**
     * Sets the businessObjectService attribute value.
     *
     * @param businessObjectService The businessObjectService to set.
     */
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    /**
     * Sets the dateTimeService attribute value.
     *
     * @param dateTimeService The dateTimeService to set.
     */
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    /**
     * Sets the parameterService attribute value.
     *
     * @param parameterService The parameterService to set.
     */
    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    /**
     * Sets the laborLedgerBalanceService attribute value.
     *
     * @param laborLedgerBalanceService The laborLedgerBalanceService to set.
     */
    public void setLaborLedgerBalanceService(LaborLedgerBalanceService laborLedgerBalanceService) {
        this.laborLedgerBalanceService = laborLedgerBalanceService;
    }

    /**
     * Sets the optionsService attribute value.
     *
     * @param optionsService The optionsService to set.
     */
    public void setOptionsService(OptionsService optionsService) {
        this.optionsService = optionsService;
    }

    /**
     * Sets the batchFileDirectoryName attribute value.
     *
     * @param batchFileDirectoryName The batchFileDirectoryName to set.
     */
    public void setBatchFileDirectoryName(String batchFileDirectoryName) {
        this.batchFileDirectoryName = batchFileDirectoryName;
    }

    /**
     * Sets the laborOutputSummaryReportWriterService attribute value.
     *
     * @param laborOutputSummaryReportWriterService The laborOutputSummaryReportWriterService to set.
     */
    public void setLaborBalanceForwardReportWriterService(ReportWriterService laborBalanceForwardReportWriterService) {
        this.laborBalanceForwardReportWriterService = laborBalanceForwardReportWriterService;
    }
}
