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

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.businessobject.Entry;
import org.kuali.kfs.gl.businessobject.OriginEntryFull;
import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
import org.kuali.kfs.gl.businessobject.PendingEntrySummary;
import org.kuali.kfs.gl.report.LedgerSummaryReport;
import org.kuali.kfs.gl.service.NightlyOutService;
import org.kuali.kfs.gl.service.OriginEntryGroupService;
import org.kuali.kfs.gl.service.OriginEntryService;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
import org.kuali.kfs.sys.service.ReportWriterService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.core.web.format.CurrencyFormatter;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * This class implements the nightly out batch job.
 */
@Transactional
public class NightlyOutServiceImpl implements NightlyOutService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(NightlyOutServiceImpl.class);

    private GeneralLedgerPendingEntryService generalLedgerPendingEntryService;
    private OriginEntryService originEntryService;
    private DateTimeService dateTimeService;
    private OriginEntryGroupService originEntryGroupService;
    private String batchFileDirectoryName;
    private ReportWriterService pendingEntryListReportWriterService;
    private ReportWriterService pendingEntrySummaryReportWriterService;
    private DataDictionaryService dataDictionaryService;

    /**
     * Constructs a NightlyOutServiceImpl instance
     */
    public NightlyOutServiceImpl() {
    }

    /**
     * Deletes all the pending general ledger entries that have now been copied to origin entries
     *
     * @see org.kuali.kfs.gl.service.NightlyOutService#deleteCopiedPendingLedgerEntries()
     */
    public void deleteCopiedPendingLedgerEntries() {
        LOG.debug("deleteCopiedPendingLedgerEntries() started");

        generalLedgerPendingEntryService.deleteByFinancialDocumentApprovedCode(KFSConstants.PENDING_ENTRY_APPROVED_STATUS_CODE.PROCESSED);
    }

    /**
     * Copies the approved pending ledger entries to origin entry table and generates a report
     *
     * @see org.kuali.kfs.gl.service.NightlyOutService#copyApprovedPendingLedgerEntries()
     */
    public void copyApprovedPendingLedgerEntries() {
        if (LOG.isInfoEnabled()) {
            LOG.info("copyApprovedPendingLedgerEntries() started");
        }
        Date today = new Date(dateTimeService.getCurrentTimestamp().getTime());

        Iterator pendingEntries = generalLedgerPendingEntryService.findApprovedPendingLedgerEntries();
        String outputFile = batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.NIGHTLY_OUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        PrintStream outputFilePs = null;

        try {
            outputFilePs = new PrintStream(outputFile);
        } catch (IOException ioe) {
            throw new RuntimeException("Cannot open output file " + outputFile + " for writing", ioe);
        }

        EntryListReport entryListReport = new EntryListReport();
        LedgerSummaryReport nightlyOutLedgerSummaryReport = new LedgerSummaryReport();

        Collection<OriginEntryFull> group = new ArrayList<OriginEntryFull>();
        while (pendingEntries.hasNext()) {
            // get one pending entry
            GeneralLedgerPendingEntry pendingEntry = (GeneralLedgerPendingEntry) pendingEntries.next();

            OriginEntryFull entry = new OriginEntryFull(pendingEntry);

            // write entry to reports
            entryListReport.writeEntry(entry, pendingEntryListReportWriterService);
            nightlyOutLedgerSummaryReport.summarizeEntry(entry);

            group.add(entry);

            // copy the pending entry to text file
            outputFilePs.printf("%s\n", entry.getLine());

            // update the pending entry to indicate it has been copied
            pendingEntry.setFinancialDocumentApprovedCode(KFSConstants.PENDING_ENTRY_APPROVED_STATUS_CODE.PROCESSED);
            pendingEntry.setTransactionDate(today);

            generalLedgerPendingEntryService.save(pendingEntry);
        }

        outputFilePs.close();

        //create done file
        String doneFileName = outputFile.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();
            }
        }

        // finish writing reports
        entryListReport.writeReportFooter(pendingEntryListReportWriterService);
        nightlyOutLedgerSummaryReport.writeReport(pendingEntrySummaryReportWriterService);
    }


    public void setGeneralLedgerPendingEntryService(GeneralLedgerPendingEntryService generalLedgerPendingEntryService) {
        this.generalLedgerPendingEntryService = generalLedgerPendingEntryService;
    }

    public void setOriginEntryService(OriginEntryService originEntryService) {
        this.originEntryService = originEntryService;
    }

    public void setOriginEntryGroupService(OriginEntryGroupService originEntryGroupService) {
        this.originEntryGroupService = originEntryGroupService;
    }

    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    public void setBatchFileDirectoryName(String batchFileDirectoryName) {
        this.batchFileDirectoryName = batchFileDirectoryName;
    }

    /**
     * Gets the pendingEntryListReportWriterService attribute.
     *
     * @return Returns the pendingEntryListReportWriterService.
     */
    public ReportWriterService getPendingEntryListReportWriterService() {
        return pendingEntryListReportWriterService;
    }

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

    /**
     * Gets the pendingEntrySummaryReportWriterService attribute.
     *
     * @return Returns the pendingEntrySummaryReportWriterService.
     */
    public ReportWriterService getPendingEntrySummaryReportWriterService() {
        return pendingEntrySummaryReportWriterService;
    }

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

    /**
     * Gets the dataDictionaryService attribute.
     *
     * @return Returns the dataDictionaryService.
     */
    public DataDictionaryService getDataDictionaryService() {
        return dataDictionaryService;
    }

    /**
     * Sets the dataDictionaryService attribute value.
     *
     * @param dataDictionaryService The dataDictionaryService to set.
     */
    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
        this.dataDictionaryService = dataDictionaryService;
    }

    /**
     * A helper class which writes out the nightly out entry list report
     */
    protected class EntryListReport {
        private PendingEntrySummary pendingEntrySummary;
        private EntryReportTotalLine totalLine;
        private Map<String, EntryReportDocumentTypeTotalLine> documentTypeTotals;
        private EntryReportDocumentNumberTotalLine documentNumberTotal;
        private int entryCount = 0;
        private String suppressKey = "";

        /**
         * Constructs a NightlyOutServiceImpl
         */
        public EntryListReport() {
            pendingEntrySummary = new PendingEntrySummary();
            totalLine = new EntryReportTotalLine();
            documentTypeTotals = new LinkedHashMap<String, EntryReportDocumentTypeTotalLine>();
        }

        /**
         * Writes an entry to the list report
         *
         * @param entry               the entry to write
         * @param reportWriterService the reportWriterService to write the entry to
         */
        public void writeEntry(OriginEntryInformation entry, ReportWriterService reportWriterService) {
            pendingEntrySummary.setOriginEntry(entry);
            if (pendingEntrySummary.getSuppressableFieldsAsKey().equals(suppressKey)) {
                pendingEntrySummary.suppressCommonFields(true);
            } else if (StringUtils.isNotBlank(suppressKey)) {
                writeDocumentTotalLine(documentNumberTotal, reportWriterService);
                documentNumberTotal = new EntryReportDocumentNumberTotalLine(pendingEntrySummary.getConstantDocumentNumber());
            }

            if (StringUtils.isBlank(suppressKey)) {
                documentNumberTotal = new EntryReportDocumentNumberTotalLine(pendingEntrySummary.getConstantDocumentNumber());
                reportWriterService.writeTableHeader(pendingEntrySummary);
            }
            suppressKey = pendingEntrySummary.getSuppressableFieldsAsKey();

            reportWriterService.writeTableRow(pendingEntrySummary);

            addPendingEntryToDocumentType(pendingEntrySummary, documentTypeTotals);
            addSummaryToTotal(pendingEntrySummary, documentNumberTotal);
            addSummaryToTotal(pendingEntrySummary, totalLine);
            entryCount += 1;
        }

        /**
         * Adds the given pending entry summary to the appropriate doc type's line total
         *
         * @param pendingEntrySummary the pending entry summary to add
         * @param docTypeTotals       the Map of doc type line total helpers to add the summary to
         */
        protected void addPendingEntryToDocumentType(PendingEntrySummary pendingEntrySummary, Map<String, EntryReportDocumentTypeTotalLine> docTypeTotals) {
            EntryReportDocumentTypeTotalLine docTypeTotal = docTypeTotals.get(pendingEntrySummary.getConstantDocumentTypeCode());
            if (docTypeTotal == null) {
                docTypeTotal = new EntryReportDocumentTypeTotalLine(pendingEntrySummary.getConstantDocumentTypeCode());
                docTypeTotals.put(pendingEntrySummary.getConstantDocumentTypeCode(), docTypeTotal);
            }
            addSummaryToTotal(pendingEntrySummary, docTypeTotal);
        }


        /**
         * Adds the given summary to the correct credit, debit, or budget total in the total line
         *
         * @param pendingEntrySummary the summary to add
         * @param totalLine           the entry report total line which holds debit, credit, and budget sum totals
         */
        protected void addSummaryToTotal(PendingEntrySummary pendingEntrySummary, EntryReportTotalLine totalLine) {
            if (pendingEntrySummary.getDebitAmount() != null) {
                totalLine.addDebitAmount(pendingEntrySummary.getDebitAmount());
            }
            if (pendingEntrySummary.getCreditAmount() != null) {
                totalLine.addCreditAmount(pendingEntrySummary.getCreditAmount());
            }
            if (pendingEntrySummary.getBudgetAmount() != null) {
                totalLine.addBudgetAmount(pendingEntrySummary.getBudgetAmount());
            }
        }

        /**
         * Writes totals for the document number we just finished writing out
         *
         * @param documentNumberTotal EntryReportDocumentNumberTotalLine containing totals to write
         * @param reportWriterService ReportWriterService for writing output to report
         */
        protected void writeDocumentTotalLine(EntryReportDocumentNumberTotalLine documentNumberTotal, ReportWriterService reportWriterService) {
            final CurrencyFormatter formatter = new CurrencyFormatter();
            final int amountLength = getDataDictionaryService().getAttributeMaxLength(Entry.class, KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_AMOUNT);

            reportWriterService.writeNewLines(1);
            reportWriterService.writeFormattedMessageLine("                                          Total: %" + amountLength + "s %" + amountLength + "s %" + amountLength + "s", formatter.format(documentNumberTotal.getCreditAmount()), formatter.format(documentNumberTotal.getDebitAmount()), formatter.format(documentNumberTotal.getBudgetAmount()));
            reportWriterService.writeNewLines(1);
        }

        /**
         * Completes the footer summary information for the report
         *
         * @param reportWriterService the reportWriterService to write the footer to
         */
        public void writeReportFooter(ReportWriterService reportWriterService) {
            // write the last total line for the last entry, because we have yet to write it
            if (documentNumberTotal != null) {
                // dNT may have been null if no entries were processed for the batch
                writeDocumentTotalLine(documentNumberTotal, reportWriterService);
            }

            final CurrencyFormatter formatter = new CurrencyFormatter();
            final int amountLength = getDataDictionaryService().getAttributeMaxLength(Entry.class, KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_AMOUNT);

            reportWriterService.writeNewLines(1);
            for (String documentTypeCode : documentTypeTotals.keySet()) {
                final EntryReportDocumentTypeTotalLine docTypeTotal = documentTypeTotals.get(documentTypeCode);
                reportWriterService.writeFormattedMessageLine("       Totals for Document Type %4s Cnt %6d: %" + amountLength + "s %" + amountLength + "s %" + amountLength + "s", documentTypeCode, docTypeTotal.getEntryCount(), formatter.format(docTypeTotal.getCreditAmount()), formatter.format(docTypeTotal.getDebitAmount()), formatter.format(docTypeTotal.getBudgetAmount()));
            }

            reportWriterService.writeNewLines(1);
            reportWriterService.writeFormattedMessageLine("                        Grand Totals Cnt %6d: %" + amountLength + "s %" + amountLength + "s %" + amountLength + "s", new Integer(entryCount), formatter.format(totalLine.getCreditAmount()), formatter.format(totalLine.getDebitAmount()), formatter.format(totalLine.getBudgetAmount()));
        }

        /**
         * Summarizes entries for the pending entry view
         */
        protected class EntryReportTotalLine {
            private KualiDecimal debitAmount = new KualiDecimal("0");
            private KualiDecimal creditAmount = new KualiDecimal("0");
            private KualiDecimal budgetAmount = new KualiDecimal("0");

            /**
             * @return the debit total
             */
            public KualiDecimal getDebitAmount() {
                return debitAmount;
            }

            /**
             * @return the credit total
             */
            public KualiDecimal getCreditAmount() {
                return creditAmount;
            }

            /**
             * @return the budget total
             */
            public KualiDecimal getBudgetAmount() {
                return budgetAmount;
            }

            /**
             * Adds the given amount to the debit total
             *
             * @param debitAmount the amount to add to the debit total
             */
            public void addDebitAmount(KualiDecimal debitAmount) {
                this.debitAmount = this.debitAmount.add(debitAmount);
            }

            /**
             * Adds the given amount to the credit total
             *
             * @param creditAmount the amount to add to the credit total
             */
            public void addCreditAmount(KualiDecimal creditAmount) {
                this.creditAmount = this.creditAmount.add(creditAmount);
            }

            /**
             * Adds the given amount to the budget total
             *
             * @param budgetAmount the amount to add to the budget total
             */
            public void addBudgetAmount(KualiDecimal budgetAmount) {
                this.budgetAmount = this.budgetAmount.add(budgetAmount);
            }
        }

        /**
         * Summarizes pending entry data per document type
         */
        protected class EntryReportDocumentTypeTotalLine extends EntryReportTotalLine {
            private String documentTypeCode;
            private int entryCount = 0;

            /**
             * Constructs a NightlyOutServiceImpl
             *
             * @param documentTypeCode the document type code to
             */
            public EntryReportDocumentTypeTotalLine(String documentTypeCode) {
                this.documentTypeCode = documentTypeCode;
            }

            /**
             * @return the document type associated with this summarizer
             */
            public String getDocumentTypeCode() {
                return this.documentTypeCode;
            }

            /**
             * @return the number of entries associated with the current document type
             */
            public int getEntryCount() {
                return this.entryCount;
            }

            /**
             * Overridden to automagically udpate the entry count
             *
             * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addBudgetAmount(org.kuali.rice.core.api.util.type.KualiDecimal)
             */
            @Override
            public void addBudgetAmount(KualiDecimal budgetAmount) {
                super.addBudgetAmount(budgetAmount);
                entryCount += 1;
            }

            /**
             * Overridden to automagically update the entry count
             *
             * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addCreditAmount(org.kuali.rice.core.api.util.type.KualiDecimal)
             */
            @Override
            public void addCreditAmount(KualiDecimal creditAmount) {
                super.addCreditAmount(creditAmount);
                entryCount += 1;
            }

            /**
             * Overridden to automagically update the entry count
             *
             * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addDebitAmount(org.kuali.rice.core.api.util.type.KualiDecimal)
             */
            @Override
            public void addDebitAmount(KualiDecimal debitAmount) {
                super.addDebitAmount(debitAmount);
                entryCount += 1;
            }
        }

        /**
         * Summarizes pending entry data per document number
         */
        protected class EntryReportDocumentNumberTotalLine extends EntryReportTotalLine {
            private String documentNumber;
            private int entryCount = 0;

            /**
             * Constructs a NightlyOutServiceImpl
             *
             * @param documentNumber the document number to total
             */
            public EntryReportDocumentNumberTotalLine(String documentNumber) {
                this.documentNumber = documentNumber;
            }

            /**
             * @return the document number associated with this summarizer
             */
            public String getDocumentNumber() {
                return this.documentNumber;
            }

            /**
             * @return the number of entries associated with the current document number
             */
            public int getEntryCount() {
                return this.entryCount;
            }

            /**
             * Overridden to automagically udpate the entry count
             *
             * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addBudgetAmount(org.kuali.rice.core.api.util.type.KualiDecimal)
             */
            @Override
            public void addBudgetAmount(KualiDecimal budgetAmount) {
                super.addBudgetAmount(budgetAmount);
                entryCount += 1;
            }

            /**
             * Overridden to automagically update the entry count
             *
             * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addCreditAmount(org.kuali.rice.core.api.util.type.KualiDecimal)
             */
            @Override
            public void addCreditAmount(KualiDecimal creditAmount) {
                super.addCreditAmount(creditAmount);
                entryCount += 1;
            }

            /**
             * Overridden to automagically update the entry count
             *
             * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addDebitAmount(org.kuali.rice.core.api.util.type.KualiDecimal)
             */
            @Override
            public void addDebitAmount(KualiDecimal debitAmount) {
                super.addDebitAmount(debitAmount);
                entryCount += 1;
            }
        }
    }
}
