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

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.batch.dataaccess.FinancialSystemDocumentHeaderPopulationDao;
import org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService;
import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader;
import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeaderMissingFromWorkflow;
import org.kuali.kfs.sys.service.NonTransactional;
import org.kuali.rice.kew.api.document.Document;
import org.kuali.rice.kew.api.document.DocumentStatus;
import org.kuali.rice.kew.api.document.WorkflowDocumentService;
import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
import org.kuali.rice.kim.api.identity.IdentityService;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.springframework.transaction.annotation.Transactional;

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 java.util.Set;

/**
 * The base implementation of the FinancialSystemDocumentHeaderPopulationService
 */
public class FinancialSystemDocumentHeaderPopulationServiceImpl implements FinancialSystemDocumentHeaderPopulationService {
    org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FinancialSystemDocumentHeaderPopulationServiceImpl.class);

    protected WorkflowDocumentService workflowDocumentService;
    protected BusinessObjectService businessObjectService;
    protected IdentityService identityService;
    protected FinancialSystemDocumentHeaderPopulationDao financialSystemDocumentHeaderPopulationDao;

    protected volatile String systemUserPrincipalId;

    /**
     * Populates financial system document header records, at a count of batchSize at a time, until the jobRunSize number of records have been processed, skipping document headers that
     * are included in documentStatusesToPopulate if the given Set has any members at all
     *
     * @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#populateFinancialSystemDocumentHeadersFromKew(int, int, Set<DocumentStatus>)
     */
    @Override
    @NonTransactional
    public void populateFinancialSystemDocumentHeadersFromKew(int batchSize, Integer jobRunSize, Set<DocumentStatus> documentStatusesToPopulate) {
        final long startTime = System.currentTimeMillis();
        for (Collection<FinancialSystemDocumentHeader> documentHeaderBatch : getFinancialSystemDocumentHeaderBatchIterable(batchSize, jobRunSize)) {
            Map<String, FinancialSystemDocumentHeader> documentHeaderMap = convertDocumentHeaderBatchToMap(documentHeaderBatch);
            handleBatch(documentHeaderMap, documentStatusesToPopulate);
        }
        final long endTime = System.currentTimeMillis();
        final double runTimeSeconds = (endTime - startTime) / 1000.0;
        LOG.info("Run time: " + runTimeSeconds);
    }

    /**
     * Reads in the matching KEW document headers for the given batch of FinancialSystemDocumentHeader records and updates
     *
     * @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#handleBatch(java.util.Map, Set<DocumentStatus>)
     */
    @Override
    @Transactional
    public void handleBatch(Map<String, FinancialSystemDocumentHeader> documentHeaders, Set<DocumentStatus> documentStatusesToPopulate) {
        List<Document> workflowDocuments = getWorkflowDocuments(documentHeaders, documentStatusesToPopulate);
        List<FinancialSystemDocumentHeader> documentHeadersToSave = new ArrayList<FinancialSystemDocumentHeader>();

        for (Document kewDocHeader : workflowDocuments) {
            final FinancialSystemDocumentHeader fsDocHeader = documentHeaders.get(kewDocHeader.getDocumentId());
            if (fsDocHeader != null) {
                updateDocumentHeader(fsDocHeader, kewDocHeader);
                documentHeadersToSave.add(fsDocHeader);
            } else {
                // how would this even happen????
                LOG.error("Document ID: " + kewDocHeader.getDocumentId() + " was returned from search but no financial system document header could be found in the map.  And it's freaking me out, man!");

            }
        }
        // save the changes
        getBusinessObjectService().save(documentHeadersToSave);
    }

    /**
     * Returns a List of KEW document headers to match the given FinancialSystemDocumentHeader records.  If a workflow document header cannot be found
     * for a financial system document header, the document number will be saved as a FinancialSystemDocumentHeaderMissingFromWorkflow record and skipped
     * from subsequent runs of the job
     *
     * @param documentHeaders            a Map of FS document headers
     * @param documentStatusesToPopulate if the given Set has any members, only documents in the given statuses will have their FinancialSystemDocumentHeader records populated
     * @return a List of matching workflow document header records, skipping any records included in the documentStatusesToPopulate Set if there are any members in it at all
     */
    protected List<Document> getWorkflowDocuments(Map<String, FinancialSystemDocumentHeader> documentHeaders, Set<DocumentStatus> documentStatusesToPopulate) {
        List<Document> workflowDocuments = new ArrayList<Document>();
        List<FinancialSystemDocumentHeaderMissingFromWorkflow> missingWorkflowHeaders = new ArrayList<FinancialSystemDocumentHeaderMissingFromWorkflow>();

        for (String documentNumber : documentHeaders.keySet()) {
            final Document workflowDoc = getWorkflowDocumentService().getDocument(documentNumber);
            if (workflowDoc != null && (documentStatusesToPopulate.isEmpty() || documentStatusesToPopulate.contains(workflowDoc.getStatus()))) {
                workflowDocuments.add(workflowDoc);
            } else if (workflowDoc == null) { // only record the error if we weren't supposed to skip the record...ie, if the workflow document is null
                LOG.error("Could not find a workflow document record for financial system document header #" + documentNumber);
                FinancialSystemDocumentHeaderMissingFromWorkflow missingWorkflowHeader = new FinancialSystemDocumentHeaderMissingFromWorkflow();
                missingWorkflowHeader.setDocumentNumber(documentNumber);
                missingWorkflowHeaders.add(missingWorkflowHeader);
            }
        }

        if (!missingWorkflowHeaders.isEmpty()) {
            getBusinessObjectService().save(missingWorkflowHeaders);
        }
        return workflowDocuments;
    }

    /**
     * Joins the given Set of document numbers with a pipe "|" character
     *
     * @param documentIds the document numbers to join
     * @return the joined document numbers, ready to be handed to the document search
     */
    protected String pipeDocumentIds(Set<String> documentIds) {
        return StringUtils.join(documentIds, '|');
    }

    /**
     * Creates a DocumentSearchCriteria which will look up the documents identified by the ids piped into the documentIdForSearch
     *
     * @return a DocumentSearchCriteria to look up the document search results
     */
    protected DocumentSearchCriteria buildDocumentSearchCriteria(String documentIdForSearch) {
        DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentId(documentIdForSearch);
        return criteria.build();
    }

    /**
     * @return the principal id of the system user
     */
    protected String getSystemUserPrincipalId() {
        if (StringUtils.isBlank(systemUserPrincipalId)) {
            final Principal principal = getIdentityService().getPrincipalByPrincipalName(KFSConstants.SYSTEM_USER);
            systemUserPrincipalId = principal.getPrincipalId();
        }
        return systemUserPrincipalId;
    }

    /**
     * Writes to the passed in log a message detailing the changes which will occur when the job is run outside of log mode,
     * ie what the updated document status, document type, initiator principal id, and application document status will be updated to
     *
     * @param financialSystemDocumentHeader the financial system document header which would have been updated
     * @param kewDocumentHeader             the workflow document header with the information to update with
     * @param log                           the log to write to
     */
    protected void logChanges(FinancialSystemDocumentHeader financialSystemDocumentHeader, Document kewDocumentHeader, Logger log) {
        log.info("Financial System Document Header " + financialSystemDocumentHeader.getDocumentNumber() + " KEW document header " + kewDocumentHeader.getDocumentId() +
            " Initiator Principal Id: " + kewDocumentHeader.getInitiatorPrincipalId() + " Document Type Name: " + kewDocumentHeader.getDocumentTypeName() +
            " Document Status: " + kewDocumentHeader.getStatus().getLabel() + " Application Document Status: " + kewDocumentHeader.getApplicationDocumentStatus());
    }

    /**
     * Updates the financial system document header with values from the workflow document header
     *
     * @param financialSystemDocumentHeader the financial system document header to update
     * @param kewDocumentHeader             the workflow document header with values to update the financial system document header with
     */
    protected void updateDocumentHeader(FinancialSystemDocumentHeader financialSystemDocumentHeader, Document kewDocumentHeader) {
        financialSystemDocumentHeader.setInitiatorPrincipalId(kewDocumentHeader.getInitiatorPrincipalId());
        financialSystemDocumentHeader.setWorkflowDocumentTypeName(kewDocumentHeader.getDocumentTypeName());
        financialSystemDocumentHeader.setWorkflowDocumentStatusCode(kewDocumentHeader.getStatus().getCode());
        financialSystemDocumentHeader.setApplicationDocumentStatus(kewDocumentHeader.getApplicationDocumentStatus());
        financialSystemDocumentHeader.setWorkflowCreateDate(new java.sql.Timestamp(kewDocumentHeader.getDateCreated().getMillis()));
    }

    /**
     * Convenience iterator to get batches of FinancialSystemDocumentHeader by batch size
     */
    protected class FinancialSystemDocumentHeaderBatchIterator implements Iterator<Collection<FinancialSystemDocumentHeader>> {
        protected int batchSize;
        protected int currentStartIndex = 1;
        protected int documentHeaderCount;

        protected FinancialSystemDocumentHeaderBatchIterator(int batchSize, Integer jobRunSize) {
            this.batchSize = batchSize;
            Map<String, Object> fieldValues = new HashMap<String, Object>(); // there's no "countAll" so we'll just pass in an empty Map for the count
            this.documentHeaderCount = getFinancialSystemDocumentHeaderCount();
            if (jobRunSize != null && jobRunSize.intValue() > 0 && jobRunSize.intValue() < this.documentHeaderCount) {
                this.documentHeaderCount = jobRunSize; // use jobRunSize to limit
            }
        }

        @Override
        public boolean hasNext() {
            return currentStartIndex <= documentHeaderCount;
        }

        @Override
        public Collection<FinancialSystemDocumentHeader> next() {
            int endIndex = currentStartIndex + batchSize - 1;
            if (endIndex > documentHeaderCount) {
                endIndex = documentHeaderCount;
            }

            // nota bene: it was discussed that it might be helpful to have a parameter with a specific list of document numbers to read in and convert.
            // if such a parameter were implemented, this would likely be a good place to use that logic....  The DAO shouldn't read the parameter directly, I think....
            Collection<FinancialSystemDocumentHeader> docHeaderBatch = readBatchOfFinancialSystemDocumentHeaders(currentStartIndex, endIndex);

            currentStartIndex = endIndex + 1;
            return docHeaderBatch;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("This iterator is read only; remove should not be called against it");
        }
    }

    /**
     * Counts the number of Financial System Document Header records without initiator principal id's set
     *
     * @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#getFinancialSystemDocumentHeaderCount()
     */
    @Transactional
    @Override
    public int getFinancialSystemDocumentHeaderCount() {
        return getFinancialSystemDocumentHeaderPopulationDao().countTotalFinancialSystemDocumentHeadersToProcess();
    }

    /**
     * Uses the DAO to retrieve the specified batch
     *
     * @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#readBatchOfFinancialSystemDocumentHeaders(int, int)
     */
    @Transactional
    @Override
    public Collection<FinancialSystemDocumentHeader> readBatchOfFinancialSystemDocumentHeaders(int startIndex, int endIndex) {
        return getFinancialSystemDocumentHeaderPopulationDao().getFinancialSystemDocumentHeadersForBatch(startIndex, endIndex);
    }

    /**
     * Returns a new Iterable to iterate over batches of FinancialSystemDocumentHeaders
     *
     * @param batchSize  the size of the batches to build
     * @param jobRunSize the number of records
     * @return the newly created Iterable
     */
    protected Iterable<Collection<FinancialSystemDocumentHeader>> getFinancialSystemDocumentHeaderBatchIterable(final int batchSize, final Integer jobRunSize) {
        return new Iterable<Collection<FinancialSystemDocumentHeader>>() {
            @Override
            public Iterator<Collection<FinancialSystemDocumentHeader>> iterator() {
                return new FinancialSystemDocumentHeaderBatchIterator(batchSize, jobRunSize);
            }
        };
    }

    /**
     * Converts a Collection of FinancialSystemDocumentHeader records into a Map keyed by the document number
     *
     * @param documentHeaderBatch a Collection of FinancialSystemDocumentHeader records
     * @return the Map of FinancialSystemDocumentHeader records keyed by document number
     */
    protected Map<String, FinancialSystemDocumentHeader> convertDocumentHeaderBatchToMap(Collection<FinancialSystemDocumentHeader> documentHeaderBatch) {
        Map<String, FinancialSystemDocumentHeader> documentHeaderMap = new HashMap<String, FinancialSystemDocumentHeader>();
        for (FinancialSystemDocumentHeader docHeader : documentHeaderBatch) {
            documentHeaderMap.put(docHeader.getDocumentNumber(), docHeader);
        }
        return documentHeaderMap;
    }

    @NonTransactional
    public WorkflowDocumentService getWorkflowDocumentService() {
        return workflowDocumentService;
    }

    @NonTransactional
    public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
        this.workflowDocumentService = workflowDocumentService;
    }

    @NonTransactional
    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    @NonTransactional
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    @NonTransactional
    public IdentityService getIdentityService() {
        return identityService;
    }

    @NonTransactional
    public void setIdentityService(IdentityService identityService) {
        this.identityService = identityService;
    }

    @NonTransactional
    public FinancialSystemDocumentHeaderPopulationDao getFinancialSystemDocumentHeaderPopulationDao() {
        return financialSystemDocumentHeaderPopulationDao;
    }

    @NonTransactional
    public void setFinancialSystemDocumentHeaderPopulationDao(FinancialSystemDocumentHeaderPopulationDao financialSystemDocumentHeaderPopulationDao) {
        this.financialSystemDocumentHeaderPopulationDao = financialSystemDocumentHeaderPopulationDao;
    }
}
