/*
 * 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.commons.lang.StringUtils;
import org.kuali.kfs.gl.batch.service.impl.DocumentGroupData;
import org.kuali.kfs.gl.batch.service.impl.OriginEntryFileIterator;
import org.kuali.kfs.gl.batch.service.impl.OriginEntryTotals;
import org.kuali.kfs.gl.businessobject.CollectorDetail;
import org.kuali.kfs.gl.businessobject.OriginEntryFull;
import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
import org.kuali.kfs.gl.report.CollectorReportData;
import org.kuali.kfs.gl.service.ScrubberService;
import org.kuali.kfs.gl.service.impl.CollectorScrubberStatus;
import org.kuali.kfs.gl.service.impl.ScrubberStatus;
import org.kuali.kfs.kns.bo.Step;
import org.kuali.kfs.krad.service.PersistenceService;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.batch.BatchSpringContext;
import org.kuali.kfs.sys.context.ProxyUtils;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;

import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;


/**
 * This class scrubs the billing details in a collector batch. Note that all services used by this class are passed in as parameters
 * to the constructor. NOTE: IT IS IMPERATIVE that a new instance of this class is constructed and used to parse each batch. Sharing
 * instances to scrub multiple batches may lead to unpredictable results.
 */
public class CollectorScrubberProcess {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CollectorScrubberProcess.class);

    protected CollectorBatch batch;
    protected String inputFileName;
    protected String validFileName;
    protected String errorFileName;
    protected String expiredFileName;
    protected ConfigurationService kualiConfigurationService;
    protected PersistenceService persistenceService;
    protected CollectorReportData collectorReportData;
    protected ScrubberService scrubberService;
    protected DateTimeService dateTimeService;

    protected Set<DocumentGroupData> errorDocumentGroups;
    protected String collectorFileDirectoryName;

    /**
     * Constructs a CollectorScrubberProcess.java.
     *
     * @param batch                     the batch to scrub
     * @param inputGroup                the origin entry group that holds all of the origin entries coming from the parsed input groups in the
     *                                  given batch
     * @param validGroup                the origin entry group that holds all of the origin entries coming that are in the origin entry scrubber
     *                                  valid group
     * @param errorGroup                the origin entry group that holds all of the origin entries coming that are in the origin entry scrubber
     *                                  error group
     * @param expiredGroup              are in the origin entry scrubber valid group that are in the origin entry scrubber expired group
     * @param originEntryService        the origin entry service that holds the origin entries in the batch (not necessarily the default
     *                                  implementation)
     * @param originEntryGroupService   the origin entry group service that holds the 3 group parameters (not necessarily the default
     *                                  implementation)
     * @param kualiConfigurationService the config service
     * @param persistenceService        the persistence service
     */
    public CollectorScrubberProcess(CollectorBatch batch, ConfigurationService kualiConfigurationService, PersistenceService persistenceService, ScrubberService scrubberService, CollectorReportData collectorReportData, DateTimeService dateTimeService, String collectorFileDirectoryName) {
        this.batch = batch;
        this.kualiConfigurationService = kualiConfigurationService;
        this.persistenceService = persistenceService;
        this.collectorReportData = collectorReportData;
        this.scrubberService = scrubberService;
        this.dateTimeService = dateTimeService;
        this.collectorFileDirectoryName = collectorFileDirectoryName;
    }

    /**
     * Scrubs the entries read in by the Collector
     *
     * @return a CollectorScrubberStatus object encapsulating the results of the scrubbing process
     */
    public CollectorScrubberStatus scrub() {
        // for the collector origin entry group scrub, we make sure that we're using a custom impl of the origin entry service and
        // group service.
        ScrubberStatus scrubberStatus = new ScrubberStatus();
        Step step = BatchSpringContext.getStep(CollectorScrubberStep.STEP_NAME);
        CollectorScrubberStep collectorStep = (CollectorScrubberStep) ProxyUtils.getTargetIfProxied(step);
        collectorStep.setScrubberStatus(scrubberStatus);
        collectorStep.setBatch(batch);
        collectorStep.setCollectorReportData(collectorReportData);

        try {
            step.execute(getClass().getName(), dateTimeService.getCurrentDate());
        } catch (Exception e) {
            LOG.error("Exception occured executing step", e);
            throw new RuntimeException("Exception occured executing step", e);
        }

        CollectorScrubberStatus collectorScrubberStatus = new CollectorScrubberStatus();
        // extract the group BOs form the scrubber

        // the FileName that contains all of the origin entries from the collector file
        inputFileName = scrubberStatus.getInputFileName();
        collectorScrubberStatus.setInputFileName(inputFileName);

        // the FileName that contains all of the origin entries from the scrubber valid FileName
        validFileName = scrubberStatus.getValidFileName();
        collectorScrubberStatus.setValidFileName(validFileName);

        // the FileName that contains all of the origin entries from the scrubber error FileName
        errorFileName = scrubberStatus.getErrorFileName();
        collectorScrubberStatus.setErrorFileName(errorFileName);

        // the FileName that contains all of the origin entries from the scrubber expired FileName (expired accounts)
        expiredFileName = scrubberStatus.getExpiredFileName();
        collectorScrubberStatus.setExpiredFileName(expiredFileName);

        retrieveErrorDocumentGroups();

        retrieveTotalsOnInputOriginEntriesAssociatedWithErrorGroup();

        removeInterDepartmentalBillingAssociatedWithErrorGroup();

        applyChangesToDetailsFromScrubberEdits(scrubberStatus.getUnscrubbedToScrubbedEntries());

        return collectorScrubberStatus;
    }

    /**
     * Removes Collector IB details not associated with entries in the Collector data
     */
    protected void removeInterDepartmentalBillingNotAssociatedWithInputEntries() {
        Iterator<CollectorDetail> detailIter = batch.getCollectorDetails().iterator();
        while (detailIter.hasNext()) {
            CollectorDetail detail = detailIter.next();
            for (OriginEntryFull inputEntry : batch.getOriginEntries()) {
                if (!isOriginEntryRelatedToDetailRecord(inputEntry, detail)) {
                    // TODO: add reporting data
                    detailIter.remove();
                }
            }
        }
    }

    /**
     * This method's purpose is similar to the scrubber's demerger. This method scans through all of the origin entries and removes
     * those billing details that share the same doc number, doc type, and origination code
     */
    protected void removeInterDepartmentalBillingAssociatedWithErrorGroup() {
        int numDetailDeleted = 0;
        Iterator<CollectorDetail> detailIter = batch.getCollectorDetails().iterator();
        while (detailIter.hasNext()) {
            CollectorDetail detail = detailIter.next();
            for (DocumentGroupData errorDocumentGroup : errorDocumentGroups) {
                if (errorDocumentGroup.matchesCollectorDetail(detail)) {
                    numDetailDeleted++;
                    detailIter.remove();
                }
            }
        }

        collectorReportData.setNumDetailDeleted(batch, new Integer(numDetailDeleted));
    }

    /**
     * Determines if an origin entry is related to the given Collector detail record
     *
     * @param originEntry the origin entry to check
     * @param detail      the Collector detail to check against
     * @return true if the origin entry is related, false otherwise
     */
    protected boolean isOriginEntryRelatedToDetailRecord(OriginEntryInformation originEntry, CollectorDetail detail) {
        return StringUtils.equals(originEntry.getUniversityFiscalPeriodCode(), detail.getUniversityFiscalPeriodCode())
            && originEntry.getUniversityFiscalYear() != null && originEntry.getUniversityFiscalYear().equals(detail.getUniversityFiscalYear())
            && StringUtils.equals(originEntry.getChartOfAccountsCode(), detail.getChartOfAccountsCode())
            && StringUtils.equals(originEntry.getAccountNumber(), detail.getAccountNumber())
            && StringUtils.equals(originEntry.getSubAccountNumber(), detail.getSubAccountNumber())
            && StringUtils.equals(originEntry.getFinancialObjectCode(), detail.getFinancialObjectCode())
            && StringUtils.equals(originEntry.getFinancialSubObjectCode(), detail.getFinancialSubObjectCode())
            && StringUtils.equals(originEntry.getFinancialSystemOriginationCode(), detail.getFinancialSystemOriginationCode())
            && StringUtils.equals(originEntry.getFinancialDocumentTypeCode(), detail.getFinancialDocumentTypeCode())
            && StringUtils.equals(originEntry.getDocumentNumber(), detail.getDocumentNumber())
            && StringUtils.equals(originEntry.getFinancialObjectTypeCode(), detail.getFinancialObjectTypeCode());
    }

    /**
     * Determines if one of the messages in the given list of errors is a fatal message
     *
     * @param errors a List of errors generated by the scrubber
     * @return true if one of the errors was fatal, false otherwise
     */
    private boolean hasFatal(List<Message> errors) {
        for (Iterator<Message> iter = errors.iterator(); iter.hasNext(); ) {
            Message element = iter.next();
            if (element.getType() == Message.TYPE_FATAL) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines if any of the error messages in the given list are warnings
     *
     * @param errors a list of errors generated by the Scrubber
     * @return true if there are any warnings in the list, false otherwise
     */
    private boolean hasWarning(List<Message> errors) {
        for (Iterator<Message> iter = errors.iterator(); iter.hasNext(); ) {
            Message element = iter.next();
            if (element.getType() == Message.TYPE_WARNING) {
                return true;
            }
        }
        return false;
    }

    /**
     * Updates the Collector detail with the data from a scrubbed entry
     *
     * @param originEntry a scrubbed origin entry
     * @param detail      a Collector detail to update
     */
    protected void applyScrubberEditsToDetail(OriginEntryInformation originEntry, CollectorDetail detail) {
        detail.setUniversityFiscalPeriodCode(originEntry.getUniversityFiscalPeriodCode());
        detail.setUniversityFiscalYear(originEntry.getUniversityFiscalYear());
        detail.setChartOfAccountsCode(originEntry.getChartOfAccountsCode());
        detail.setAccountNumber(originEntry.getAccountNumber());
        detail.setSubAccountNumber(originEntry.getSubAccountNumber());
        detail.setFinancialObjectCode(originEntry.getFinancialObjectCode());
        detail.setFinancialSubObjectCode(originEntry.getFinancialSubObjectCode());
        detail.setFinancialSystemOriginationCode(originEntry.getFinancialSystemOriginationCode());
        detail.setFinancialDocumentTypeCode(originEntry.getFinancialDocumentTypeCode());
        detail.setDocumentNumber(originEntry.getDocumentNumber());
        detail.setFinancialBalanceTypeCode(originEntry.getFinancialBalanceTypeCode());
        detail.setFinancialObjectTypeCode(originEntry.getFinancialObjectTypeCode());
    }

    /**
     * Updates all Collector details with the data from scrubbed origin entries
     *
     * @param unscrubbedToScrubbedEntries a Map relating original origin entries to scrubbed origin entries
     */
    protected void applyChangesToDetailsFromScrubberEdits(Map<OriginEntryInformation, OriginEntryInformation> unscrubbedToScrubbedEntries) {
        Set<Entry<OriginEntryInformation, OriginEntryInformation>> mappings = unscrubbedToScrubbedEntries.entrySet();

        int numDetailAccountValuesChanged = 0;
        for (CollectorDetail detail : batch.getCollectorDetails()) {
            for (Entry<OriginEntryInformation, OriginEntryInformation> mapping : mappings) {
                OriginEntryInformation originalEntry = mapping.getKey();
                OriginEntryInformation scrubbedEntry = mapping.getValue();
                // TODO: this algorithm could be made faster using a lookup table instead of a nested loop
                if (isOriginEntryRelatedToDetailRecord(originalEntry, detail)) {
                    if (!StringUtils.equals(originalEntry.getChartOfAccountsCode(), scrubbedEntry.getChartOfAccountsCode()) || !StringUtils.equals(originalEntry.getAccountNumber(), scrubbedEntry.getAccountNumber())) {
                        numDetailAccountValuesChanged++;
                    }
                    applyScrubberEditsToDetail(scrubbedEntry, detail);
                    break;
                }
            }
        }

        collectorReportData.setNumDetailAccountValuesChanged(batch, numDetailAccountValuesChanged);
    }

    /**
     * Based on the origin entries in the origin entry scrubber-produced error group, creates a set of all {@link DocumentGroupData}s
     * represented by those origin entries and initializes the {@link #errorDocumentGroups} variable
     */
    protected void retrieveErrorDocumentGroups() {
        File errorFile = new File(collectorFileDirectoryName + File.separator + errorFileName);
        OriginEntryFileIterator entryIterator = new OriginEntryFileIterator(errorFile);
        errorDocumentGroups = DocumentGroupData.getDocumentGroupDatasForTransactions(entryIterator);
    }

    /**
     * Computes the totals of the input entries that were associated with the entries in the error group, which is created in the
     * scrubber. These totals are reflected in the collector report data object.
     */
    protected void retrieveTotalsOnInputOriginEntriesAssociatedWithErrorGroup() {
        Map<DocumentGroupData, OriginEntryTotals> inputDocumentTotals = new HashMap<DocumentGroupData, OriginEntryTotals>();

        File inputFile = new File(collectorFileDirectoryName + File.separator + inputFileName);
        OriginEntryFileIterator inputIterator = new OriginEntryFileIterator(inputFile);

        while (inputIterator.hasNext()) {
            OriginEntryFull originEntryFull = (OriginEntryFull) inputIterator.next();

            DocumentGroupData inputGroupData = new DocumentGroupData(originEntryFull.getDocumentNumber(), originEntryFull.getFinancialDocumentTypeCode(), originEntryFull.getFinancialSystemOriginationCode());
            if (errorDocumentGroups.contains(inputGroupData)) {
                OriginEntryTotals originEntryTotals = new OriginEntryTotals();
                if (inputDocumentTotals.containsKey(inputGroupData)) {
                    originEntryTotals = inputDocumentTotals.get(inputGroupData);
                }

                originEntryTotals.addToTotals(originEntryFull);
                inputDocumentTotals.put(inputGroupData, originEntryTotals);
            }
        }

        collectorReportData.setTotalsOnInputOriginEntriesAssociatedWithErrorGroup(batch, inputDocumentTotals);
    }
}
