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

import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.integration.purap.PurchasingAccountsPayableModuleService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.pdp.PdpConstants;
import org.kuali.kfs.pdp.batch.service.ProcessPdpCancelPaidService;
import org.kuali.kfs.pdp.businessobject.ExtractionUnit;
import org.kuali.kfs.pdp.businessobject.PaymentDetail;
import org.kuali.kfs.pdp.service.PaymentDetailService;
import org.kuali.kfs.pdp.service.PaymentGroupService;
import org.kuali.kfs.sys.KFSParameterKeyConstants;
import org.kuali.kfs.sys.batch.service.PaymentSourceToExtractService;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.PaymentSource;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Date;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Implementation of ProcessPdpCancelPaidService
 */
@Transactional
public class ProcessPdpCancelPaidServiceImpl implements ProcessPdpCancelPaidService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ProcessPdpCancelPaidServiceImpl.class);

    protected PaymentGroupService paymentGroupService;
    protected PaymentDetailService paymentDetailService;
    protected ParameterService parameterService;
    protected DateTimeService dateTimeService;
    protected PurchasingAccountsPayableModuleService purchasingAccountsPayableModuleService;
    protected DocumentService documentService;

    protected volatile Set<String> paymentSourceCheckACHDocumentTypes;
    protected volatile List<PaymentSourceToExtractService<PaymentSource>> paymentSourceToExtractServices;

    /**
     * @see org.kuali.kfs.module.purap.service.ProcessPdpCancelPaidService#processPdpCancels()
     */
    @Override
    public void processPdpCancels() {
        LOG.debug("processPdpCancels() started");

        Date processDate = dateTimeService.getCurrentSqlDate();

        final List<ExtractionUnit> extractionUnits = getExtractionUnits();
        Iterator<PaymentDetail> details = paymentDetailService.getUnprocessedCancelledDetails(extractionUnits);
        while (details.hasNext()) {
            PaymentDetail paymentDetail = details.next();

            String documentTypeCode = paymentDetail.getFinancialDocumentTypeCode();
            String documentNumber = paymentDetail.getCustPaymentDocNbr();

            boolean primaryCancel = paymentDetail.getPrimaryCancelledPayment();
            boolean disbursedPayment = PdpConstants.PaymentStatusCodes.CANCEL_DISBURSEMENT.equals(paymentDetail.getPaymentGroup().getPaymentStatusCode());

            if (purchasingAccountsPayableModuleService.isPurchasingBatchDocument(documentTypeCode)) {
                purchasingAccountsPayableModuleService.handlePurchasingBatchCancels(documentNumber, documentTypeCode, primaryCancel, disbursedPayment);
            } else {
                PaymentSourceToExtractService<PaymentSource> extractService = getPaymentSourceToExtractService(paymentDetail);
                if (extractService != null) {
                    try {
                        PaymentSource dv = (PaymentSource) getDocumentService().getByDocumentHeaderId(documentNumber);
                        if (dv != null) {
                            if (disbursedPayment || primaryCancel) {
                                extractService.cancelPayment(dv, processDate);
                            } else {
                                extractService.resetFromExtraction(dv);
                            }
                        }
                    } catch (WorkflowException we) {
                        throw new RuntimeException("Could not retrieve document #" + documentNumber, we);
                    }
                } else {
                    LOG.warn("processPdpCancels() Unknown document type (" + documentTypeCode + ") for document ID: " + documentNumber);
                    continue;
                }
            }

            paymentGroupService.processCancelledGroup(paymentDetail.getPaymentGroup(), processDate);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.service.ProcessPdpCancelPaidService#processPdpPaids()
     */
    @Override
    public void processPdpPaids() {
        LOG.debug("processPdpPaids() started");

        Date processDate = dateTimeService.getCurrentSqlDate();

        final List<ExtractionUnit> extractionUnits = getExtractionUnits();
        Iterator<PaymentDetail> details = paymentDetailService.getUnprocessedPaidDetails(extractionUnits);
        while (details.hasNext()) {
            PaymentDetail paymentDetail = details.next();

            String documentTypeCode = paymentDetail.getFinancialDocumentTypeCode();
            String documentNumber = paymentDetail.getCustPaymentDocNbr();

            if (purchasingAccountsPayableModuleService.isPurchasingBatchDocument(documentTypeCode)) {
                purchasingAccountsPayableModuleService.handlePurchasingBatchPaids(documentNumber, documentTypeCode, processDate);
            } else {
                PaymentSourceToExtractService<PaymentSource> extractService = getPaymentSourceToExtractService(paymentDetail);
                if (extractService != null) {
                    try {
                        PaymentSource dv = (PaymentSource) getDocumentService().getByDocumentHeaderId(documentNumber);
                        extractService.markAsPaid(dv, processDate);
                    } catch (WorkflowException we) {
                        throw new RuntimeException("Could not retrieve document #" + documentNumber, we);
                    }
                } else {
                    LOG.warn("processPdpPaids() Unknown document type (" + documentTypeCode + ") for document ID: " + documentNumber);
                    continue;
                }
            }

            paymentGroupService.processPaidGroup(paymentDetail.getPaymentGroup(), processDate);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.service.ProcessPdpCancelPaidService#processPdpCancelsAndPaids()
     */
    @Override
    public void processPdpCancelsAndPaids() {
        LOG.debug("processPdpCancelsAndPaids() started");

        processPdpCancels();
        processPdpPaids();
    }

    /**
     * @return a List of all available PaymentSourceToExtractService implementations
     */
    protected List<PaymentSourceToExtractService<PaymentSource>> getPaymentSourceToExtractServices() {
        if (paymentSourceToExtractServices == null) {
            paymentSourceToExtractServices = new ArrayList<PaymentSourceToExtractService<PaymentSource>>();
            Map<String, PaymentSourceToExtractService> extractionServices = SpringContext.getBeansOfType(PaymentSourceToExtractService.class);
            for (PaymentSourceToExtractService<PaymentSource> extractionService : extractionServices.values()) {
                paymentSourceToExtractServices.add(extractionService);
            }
        }
        return paymentSourceToExtractServices;
    }

    /**
     * Looks up the PaymentSourceToExtractService which can act upon the given PaymentDetail, based on the PaymentDetail's document type
     *
     * @param paymentDetail the payment detail to find an extraction service to act upon
     * @return the matching PaymentSourceToExtractService, or null if a matching service could not be found (which would be weird, because _something_ created this PaymentDetail, but...whatever)
     */
    protected PaymentSourceToExtractService<PaymentSource> getPaymentSourceToExtractService(PaymentDetail paymentDetail) {
        for (PaymentSourceToExtractService<PaymentSource> extractionService : getPaymentSourceToExtractServices()) {
            if (extractionService.handlesAchCheckDocumentType(paymentDetail.getFinancialDocumentTypeCode())) {
                return extractionService;
            }
        }
        return null;
    }

    /**
     * Loops through the PaymentSourceToExtractService List and builds ExtractionUnits for each
     *
     * @return a List of ExtractionUnits for each customer profile organization and sub-organization handled by PaymentSourceToExtractServices
     */
    protected List<ExtractionUnit> getExtractionUnitsForPaymentSourceToExtractServices() {
        List<ExtractionUnit> extractionUnits = new ArrayList<ExtractionUnit>();
        for (PaymentSourceToExtractService<PaymentSource> extractionService : getPaymentSourceToExtractServices()) {
            final ExtractionUnit extractionUnit = new ExtractionUnit(extractionService.getPreDisbursementCustomerProfileUnit(), extractionService.getPreDisbursementCustomerProfileSubUnit());
            if (!extractionUnits.contains(extractionUnit)) {
                extractionUnits.add(extractionUnit);
            }
        }
        return extractionUnits;
    }

    /**
     * @return a List of all known ExtractionUnits
     */
    protected List<ExtractionUnit> getExtractionUnits() {
        List<ExtractionUnit> extractionUnits = getExtractionUnitsForPaymentSourceToExtractServices();
        final String purapOrg = parameterService.getParameterValueAsString(KfsParameterConstants.PURCHASING_BATCH.class, KFSParameterKeyConstants.PurapPdpParameterConstants.PURAP_PDP_ORG_CODE);
        final String purapSubUnit = parameterService.getParameterValueAsString(KfsParameterConstants.PURCHASING_BATCH.class, KFSParameterKeyConstants.PurapPdpParameterConstants.PURAP_PDP_SUB_UNIT_CODE);
        final ExtractionUnit purapExtractionUnit = new ExtractionUnit(purapOrg, purapSubUnit);
        extractionUnits.add(purapExtractionUnit);
        return Collections.unmodifiableList(extractionUnits);
    }

    public void setPaymentDetailService(PaymentDetailService paymentDetailService) {
        this.paymentDetailService = paymentDetailService;
    }

    public void setPaymentGroupService(PaymentGroupService paymentGroupService) {
        this.paymentGroupService = paymentGroupService;
    }

    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    public void setPurchasingAccountsPayableModuleService(PurchasingAccountsPayableModuleService purchasingAccountsPayableModuleService) {
        this.purchasingAccountsPayableModuleService = purchasingAccountsPayableModuleService;
    }

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

    /**
     * @return the implementation of the DocumentService to use
     */
    public DocumentService getDocumentService() {
        return documentService;
    }

    /**
     * Sets the implementation of the DocumentService to use
     *
     * @param documentService the implementation of the DocumentService to use
     */
    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

}
