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

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.ErrorMessage;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.tem.TemConstants;
import org.kuali.kfs.module.tem.TemConstants.CreditCardStagingDataErrorCodes;
import org.kuali.kfs.module.tem.TemConstants.ExpenseImport;
import org.kuali.kfs.module.tem.TemConstants.ExpenseTypes;
import org.kuali.kfs.module.tem.TemKeyConstants;
import org.kuali.kfs.module.tem.TemPropertyConstants;
import org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService;
import org.kuali.kfs.module.tem.batch.service.DataReportService;
import org.kuali.kfs.module.tem.businessobject.CreditCardAgency;
import org.kuali.kfs.module.tem.businessobject.CreditCardImportData;
import org.kuali.kfs.module.tem.businessobject.CreditCardStagingData;
import org.kuali.kfs.module.tem.businessobject.ExpenseType;
import org.kuali.kfs.module.tem.businessobject.HistoricalTravelExpense;
import org.kuali.kfs.module.tem.businessobject.TemProfileAccount;
import org.kuali.kfs.module.tem.service.CreditCardAgencyService;
import org.kuali.kfs.module.tem.service.TemProfileService;
import org.kuali.kfs.module.tem.service.TravelExpenseService;
import org.kuali.kfs.sys.batch.BatchInputFileType;
import org.kuali.kfs.sys.batch.service.BatchInputFileService;
import org.kuali.kfs.sys.report.BusinessObjectReportHelper;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CreditCardDataImportServiceImpl implements CreditCardDataImportService {

    public static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CreditCardDataImportServiceImpl.class);

    public final static String REPORT_FILE_NAME_PATTERN = "{0}/{1}_{2}{3}";

    private BatchInputFileService batchInputFileService;
    private BusinessObjectService businessObjectService;
    private DateTimeService dateTimeService;
    private TemProfileService temProfileService;
    private TravelExpenseService travelExpenseService;
    private CreditCardAgencyService creditCardAgencyService;
    private DataReportService dataReportService;

    private List<BatchInputFileType> creditCardDataImportFileTypes;
    private String creditCardDataFileErrorDirectory;

    private BusinessObjectReportHelper creditCardDataUploadReportHelper;

    private String creditCardDataReportDirectory;
    private String creditCardDataReportFilePrefix;

    /**
     * @see org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService#importCreditCardData()
     */
    @Override
    public boolean importCreditCardData() {
        boolean success = true;

        for (BatchInputFileType inputFileType : creditCardDataImportFileTypes) {
            List<String> inputFileNames = batchInputFileService.listInputFileNamesWithDoneFile(inputFileType);

            for (String dataFileName : inputFileNames) {
                success &= importCreditCardDataFile(dataFileName, inputFileType);
            }
        }

        return success;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService#importCreditCardDataFile(java.lang.String, org.kuali.kfs.sys.batch.BatchInputFileType)
     */
    @Override
    public boolean importCreditCardDataFile(String dataFileName, BatchInputFileType inputFileType) {

        try {
            FileInputStream fileContents = new FileInputStream(dataFileName);

            byte[] fileByteContent = IOUtils.toByteArray(fileContents);
            CreditCardImportData creditCardData = (CreditCardImportData) batchInputFileService.parse(inputFileType, fileByteContent);
            IOUtils.closeQuietly(fileContents);

            LOG.info("Credit Card Import - validating: " + dataFileName);
            List<CreditCardStagingData> validCreditCardList = validateCreditCardData(creditCardData, dataFileName);
            if (!validCreditCardList.isEmpty()) {
                businessObjectService.save(validCreditCardList);
            }
        } catch (Exception ex) {
            LOG.error("Failed to process the file : " + dataFileName, ex);
            moveErrorFile(dataFileName, creditCardDataFileErrorDirectory);
            return false;
        } finally {
            removeDoneFiles(dataFileName);
        }
        return true;
    }

    public void moveErrorFile(String dataFileName, String creditCardDataFileErrorDirectory) {
        File dataFile = new File(dataFileName);

        if (!dataFile.exists() || !dataFile.canRead()) {
            LOG.error("Cannot find/read data file " + dataFileName);

        } else {

            try {
                FileUtils.moveToDirectory(dataFile, new File(creditCardDataFileErrorDirectory), true);
            } catch (IOException ex) {
                LOG.error("Cannot move the file:" + dataFile + " to the directory: " + creditCardDataFileErrorDirectory, ex);
            }
        }
    }


    /**
     * Clears out associated .done files for the processed data files.
     */
    protected void removeDoneFiles(String dataFileName) {
        File doneFile = new File(StringUtils.substringBeforeLast(dataFileName, ".") + TemConstants.DONE_FILE_SUFFIX);
        if (doneFile.exists()) {
            doneFile.delete();
        }

    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService#validateCreditCardData(org.kuali.kfs.module.tem.businessobject.CreditCardImportData, java.lang.String)
     */
    @Override
    public List<CreditCardStagingData> validateCreditCardData(CreditCardImportData creditCardList, String dataFileName) {

        PrintStream reportDataStream = dataReportService.getReportPrintStream(getCreditCardDataReportDirectory(), getCreditCardDataReportFilePrefix());
        List<CreditCardStagingData> validData = new ArrayList<CreditCardStagingData>();

        try {
            dataReportService.writeReportHeader(reportDataStream, dataFileName, TemKeyConstants.MESSAGE_CREDIT_CARD_DATA_REPORT_HEADER, getCreditCardDataUploadReportHelper());
            Integer count = 1;
            for (CreditCardStagingData creditCardData : creditCardList.getCreditCardData()) {
                LOG.info("Validating credit card import. Record# " + count + " of " + creditCardList.getCreditCardData().size());

                creditCardData.setErrorCode(CreditCardStagingDataErrorCodes.CREDIT_CARD_NO_ERROR);
                creditCardData.setImportBy(creditCardList.getImportBy());
                creditCardData.setStagingFileName(StringUtils.substringAfterLast(dataFileName, File.separator));

                List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();

                if (validateAndSetCreditCardAgency(creditCardData)) {

                    if (creditCardData.getExpenseImport() == ExpenseImport.traveler) {

                        TemProfileAccount temProfileAccount = findTraveler(creditCardData);
                        if (ObjectUtils.isNotNull(temProfileAccount)) {

                            //Set Traveler Id
                            if (ObjectUtils.isNull(creditCardData.getTravelerId()) || creditCardData.getTravelerId() == 0) {
                                Integer travelerId = new Integer(temProfileAccount.getProfile().getEmployeeId()).intValue();
                                creditCardData.setTravelerId(travelerId);
                            }

                            creditCardData.setTemProfileId(temProfileAccount.getProfileId());

                            //Set expense type code to O-Other if null
                            if (creditCardData.getExpenseTypeCode() == null) {
                                creditCardData.setExpenseTypeCode(ExpenseTypes.OTHER);
                            }

                            // write an error if the expense type code is not valid
                            final ExpenseType expenseType = businessObjectService.findBySinglePrimaryKey(ExpenseType.class, creditCardData.getExpenseTypeCode());
                            if (ObjectUtils.isNotNull(expenseType)) {

                                //Set Credit Card Key(traveler Id + Credit Card Agency + Credit Card number
                                creditCardData.setCreditCardKey(creditCardData.getTravelerId() + temProfileAccount.getCreditCardAgency().getCreditCardOrAgencyCode() + creditCardData.getCreditCardNumber());

                                // need to do the duplicate check at this point, since the CC key is one of the fields being checked
                                if (!isDuplicate(creditCardData, errorMessages)) {
                                    creditCardData.setMoveToHistoryIndicator(true);
                                    creditCardData.setProcessingTimestamp(dateTimeService.getCurrentTimestamp());
                                    validData.add(creditCardData);
                                }
                            } else {
                                LOG.error("Invalid expense type code " + creditCardData.getExpenseTypeCode() + " in Credit Card Data record");
                                errorMessages.add(new ErrorMessage(TemKeyConstants.MESSAGE_CREDIT_CARD_DATA_INVALID_EXPENSE_TYPE_CODE, creditCardData.getExpenseTypeCode()));
                            }
                        } else {
                            LOG.error("No traveler found for credit card number: " + creditCardData.getCreditCardNumber());
                            errorMessages.add(new ErrorMessage(TemKeyConstants.MESSAGE_CREDIT_CARD_DATA_NO_TRAVELER_FOUND, creditCardData.getCreditCardNumber()));
                        }
                    } else if (creditCardData.getExpenseImport() == ExpenseImport.trip) {

                        if (!isDuplicate(creditCardData, errorMessages)) {
                            creditCardData.setProcessingTimestamp(dateTimeService.getCurrentTimestamp());
                            validData.add(creditCardData);
                        }
                    }

                } else {
                    errorMessages.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_CREDIT_CARD_DATA_INVALID_CCA, creditCardData.getCreditCardOrAgencyCode()));
                }

                //writer to error report
                if (!errorMessages.isEmpty()) {
                    dataReportService.writeToReport(reportDataStream, creditCardData, errorMessages, getCreditCardDataUploadReportHelper());
                }

                count++;
            }
        } finally {
            if (reportDataStream != null) {
                reportDataStream.flush();
                reportDataStream.close();
            }
        }
        return validData;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService#isDuplicate(org.kuali.kfs.module.tem.businessobject.CreditCardStagingData, java.util.List)
     */
    @Override
    public boolean isDuplicate(CreditCardStagingData creditCardData, List<ErrorMessage> errorMessages) {
        Map<String, Object> fieldValues = new HashMap<String, Object>();

        if (StringUtils.isNotEmpty(creditCardData.getCreditCardKey())) {
            fieldValues.put(TemPropertyConstants.CREDIT_CARD_KEY, creditCardData.getCreditCardKey());
        }
        if (StringUtils.isNotEmpty(creditCardData.getReferenceNumber())) {
            fieldValues.put(TemPropertyConstants.REFERENCE_NUMBER, creditCardData.getReferenceNumber());
        }
        if (ObjectUtils.isNotNull(creditCardData.getTransactionAmount())) {
            fieldValues.put(TemPropertyConstants.TRANSACTION_AMOUNT, creditCardData.getTransactionAmount());
        }
        if (ObjectUtils.isNotNull(creditCardData.getTransactionDate())) {
            fieldValues.put(TemPropertyConstants.TRANSACTION_DATE, creditCardData.getTransactionDate());
        }
        if (ObjectUtils.isNotNull(creditCardData.getBankPostDate())) {
            fieldValues.put(TemPropertyConstants.BANK_POSTED_DATE, creditCardData.getBankPostDate());
        }
        if (StringUtils.isNotEmpty(creditCardData.getMerchantName())) {
            fieldValues.put(TemPropertyConstants.MERCHANT_NAME, creditCardData.getMerchantName());
        }
        List<CreditCardStagingData> creditCardDataList = (List<CreditCardStagingData>) businessObjectService.findMatching(CreditCardStagingData.class, fieldValues);

        if (ObjectUtils.isNull(creditCardDataList) || creditCardDataList.size() == 0) {
            return false;
        }
        LOG.error("Found a duplicate entry for credit card. Matching credit card id: " + creditCardDataList.get(0).getId());
        SimpleDateFormat format = new SimpleDateFormat();
        ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_CREDIT_CARD_DATA_DUPLICATE_RECORD,
            creditCardData.getCreditCardKey(), creditCardData.getReferenceNumber(), creditCardData.getTransactionAmount().toString(),
            format.format(creditCardData.getTransactionDate()), format.format(creditCardData.getBankPostDate()), creditCardData.getMerchantName());

        errorMessages.add(error);
        return true;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService#findTraveler(org.kuali.kfs.module.tem.businessobject.CreditCardStagingData)
     */
    @Override
    public TemProfileAccount findTraveler(CreditCardStagingData creditCardData) {
        Map<String, String> criteria = new HashMap<String, String>(1);
        criteria.put(TemPropertyConstants.ACCOUNT_NUMBER, creditCardData.getCreditCardNumber());

        Collection<TemProfileAccount> temProfileAccounts = businessObjectService.findMatching(TemProfileAccount.class, criteria);

        if (ObjectUtils.isNotNull(temProfileAccounts) && temProfileAccounts.size() > 0) {
            return temProfileAccounts.iterator().next();
        }

        return null;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService#validateAndSetCreditCardAgency(org.kuali.kfs.module.tem.businessobject.CreditCardStagingData)
     */
    @Override
    public boolean validateAndSetCreditCardAgency(CreditCardStagingData creditCardData) {
        CreditCardAgency ccAgency = creditCardAgencyService.getCreditCardAgencyByCode(creditCardData.getCreditCardOrAgencyCode());
        if (ObjectUtils.isNull(ccAgency)) {
            LOG.error("Mandatory Field Credit Card Or Agency Code is invalid: " + creditCardData.getCreditCardOrAgencyCode());
            creditCardData.setErrorCode(CreditCardStagingDataErrorCodes.CREDIT_CARD_INVALID_CC_AGENCY);
            return false;
        }
        creditCardData.setCreditCardAgency(ccAgency);
        creditCardData.setCreditCardOrAgencyCode(ccAgency.getCreditCardOrAgencyCode());
        return true;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.CreditCardDataImportService#moveCreditCardDataToHistoricalExpenseTable()
     */
    @Override
    public boolean moveCreditCardDataToHistoricalExpenseTable() {

        List<CreditCardStagingData> creditCardData = travelExpenseService.retrieveValidCreditCardData();
        if (ObjectUtils.isNotNull(creditCardData) && creditCardData.size() > 0) {
            for (CreditCardStagingData creditCard : creditCardData) {
                boolean result = processCreditCardStagingExpense(creditCard);
                LOG.info("Credit Card Staging Data Id: " + creditCard.getId() + (result ? " was" : " was not") + " processed.");
            }
        }

        return true;
    }

    @Transactional
    protected boolean processCreditCardStagingExpense(CreditCardStagingData creditCard) {
        LOG.info("Creating historical travel expense for credit card: " + creditCard.getId());
        HistoricalTravelExpense expense = travelExpenseService.createHistoricalTravelExpense(creditCard);
        businessObjectService.save(expense);

        //Mark as moved to historical
        creditCard.setErrorCode(CreditCardStagingDataErrorCodes.CREDIT_CARD_MOVED_TO_HISTORICAL);
        LOG.info("Finished creating historical travel expense for credit card: " + creditCard.getId() + " Historical Travel Expense: " + expense.getId());
        businessObjectService.save(creditCard);

        return true;
    }

    /**
     * Sets the batchInputFileService attribute value.
     *
     * @param batchInputFileService The batchInputFileService to set.
     */
    public void setBatchInputFileService(BatchInputFileService batchInputFileService) {
        this.batchInputFileService = batchInputFileService;
    }

    /**
     * Sets the creditCardDataImportFileTypes attribute value.
     *
     * @param creditCardDataImportFileTypes The creditCardDataImportFileTypes to set.
     */
    public void setCreditCardDataImportFileTypes(List<BatchInputFileType> creditCardDataImportFileTypes) {
        this.creditCardDataImportFileTypes = creditCardDataImportFileTypes;
    }

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

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


    /**
     * Sets the temProfileService attribute value.
     *
     * @param temProfileService The temProfileService to set.
     */
    public void setTemProfileService(TemProfileService temProfileService) {
        this.temProfileService = temProfileService;
    }

    /**
     * This method...
     *
     * @param argTravelExpenseService
     */
    public void setTravelExpenseService(TravelExpenseService argTravelExpenseService) {
        this.travelExpenseService = argTravelExpenseService;
    }

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

    public void setCreditCardAgencyService(CreditCardAgencyService creditCardAgencyService) {
        this.creditCardAgencyService = creditCardAgencyService;
    }

    public void setCreditCardDataUploadReportHelper(BusinessObjectReportHelper creditCardDataUploadReportHelper) {
        this.creditCardDataUploadReportHelper = creditCardDataUploadReportHelper;
    }

    public void setCreditCardDataReportDirectory(String creditCardDataReportDirectory) {
        this.creditCardDataReportDirectory = creditCardDataReportDirectory;
    }

    public void setCreditCardDataReportFilePrefix(String creditCardDataReportFilePrefix) {
        this.creditCardDataReportFilePrefix = creditCardDataReportFilePrefix;
    }

    public BusinessObjectReportHelper getCreditCardDataUploadReportHelper() {
        return creditCardDataUploadReportHelper;
    }

    public String getCreditCardDataReportDirectory() {
        return creditCardDataReportDirectory;
    }

    public String getCreditCardDataReportFilePrefix() {
        return creditCardDataReportFilePrefix;
    }

    public DataReportService getDataReportService() {
        return dataReportService;
    }

    public void setDataReportService(DataReportService dataReportService) {
        this.dataReportService = dataReportService;
    }
}
