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

import com.lowagie.text.Chunk;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfWriter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.xerces.dom.DocumentImpl;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.kuali.kfs.module.ar.ArConstants;
import org.kuali.kfs.module.ar.ArPropertyConstants;
import org.kuali.kfs.module.ar.batch.service.CustomerInvoiceWriteoffBatchService;
import org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO;
import org.kuali.kfs.module.ar.businessobject.Customer;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceWriteoffDocumentService;
import org.kuali.kfs.module.ar.document.service.CustomerService;
import org.kuali.kfs.sys.batch.BatchInputFileType;
import org.kuali.kfs.sys.batch.service.BatchInputFileService;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.exception.ParseException;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.kim.api.identity.entity.Entity;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.awt.*;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

@Transactional
public class CustomerInvoiceWriteoffBatchServiceImpl implements CustomerInvoiceWriteoffBatchService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CustomerInvoiceWriteoffBatchServiceImpl.class);

    private static final String XML_ROOT_ELEMENT_NAME = "invoiceWriteoffBatch";
    private static final String XML_BATCH_NAMESPACE = "http://www.kuali.org/kfs/ar/customerInvoiceWriteoffBatch";
    private static final String BATCH_FILE_KEY = "BATCH-FILE";
    private static final String WORKFLOW_DOC_ID_PREFIX = " - WITH WORKFLOW DOCID: ";

    private CustomerService customerService;
    private CustomerInvoiceDocumentService invoiceDocumentService;
    private DateTimeService dateTimeService;
    private BatchInputFileService batchInputFileService;
    private BatchInputFileType batchInputFileType;
    private String reportsDirectory;

    public CustomerInvoiceWriteoffBatchServiceImpl() {
    }

    @Override
    public boolean loadFiles() {
        LOG.info("Beginning processing of all available files for AR Customer Invoice Writeoff Batch Documents.");

        boolean result = true;

        //  create a list of the files to process
        List<String> fileNamesToLoad = getListOfFilesToProcess();
        LOG.info("Found " + fileNamesToLoad.size() + " file(s) to process.");
        boolean anyFilesFound = (fileNamesToLoad.size() > 0);

        //  create the pdf doc
        com.lowagie.text.Document pdfdoc = null;
        //  process each file in turn
        List<String> processedFiles;
        try {
            if (anyFilesFound) {
                pdfdoc = getPdfDoc();
            }
            try {
                processedFiles = new ArrayList<String>();
                for (String inputFileName : fileNamesToLoad) {

                    LOG.info("Beginning processing of filename: " + inputFileName + ".");

                    //  setup the results reporting
                    writeFileNameSectionTitle(pdfdoc, inputFileName);

                    //  load the file
                    boolean success = false;
                    try {
                        success = loadFile(inputFileName, pdfdoc);
                    } catch (Exception e) {
                        LOG.error("An unhandled error occurred.  " + e.getMessage());
                        writeInvoiceSectionMessage(pdfdoc, "ERROR - Unhandled exception caught.");
                        writeInvoiceSectionMessage(pdfdoc, e.getMessage());
                    }
                    result &= success;

                    //  handle result
                    if (success) {
                        result &= true;
                        writeInvoiceSectionMessage(pdfdoc, "File successfully completed processing.");
                        processedFiles.add(inputFileName);
                    } else {
                        writeInvoiceSectionMessage(pdfdoc, "File failed to process successfully.");
                        result &= false;
                    }
                }
            } finally {
                //  if we've written anything, then spool it out to the file
                if (pdfdoc != null) {
                    pdfdoc.close();
                }
            }

            //  remove done files
            removeDoneFiles(processedFiles);
        } catch (IOException | DocumentException ex) {
            throw new RuntimeException("Could not load customer invoice writeoff files", ex);
        }

        return result;
    }

    /**
     * Clears out associated .done files for the processed data files.
     */
    protected void removeDoneFiles(List<String> dataFileNames) {
        for (String dataFileName : dataFileNames) {
            String doneFileName = doneFileName(dataFileName);
            File doneFile = new File(doneFileName);
            if (doneFile.exists()) {
                doneFile.delete();
            }
        }
    }

    public boolean loadFile(String fileName, com.lowagie.text.Document pdfdoc) {

        boolean result = true;

        //  load up the file into a byte array
        byte[] fileByteContent = safelyLoadFileBytes(fileName);

        //  parse the file against the XSD schema and load it into an object
        LOG.info("Attempting to parse the file using Apache Digester.");
        Object parsedObject = null;
        try {
            parsedObject = batchInputFileService.parse(batchInputFileType, fileByteContent);
        } catch (ParseException e) {
            LOG.error("Error parsing batch file: " + e.getMessage());
            writeInvoiceSectionMessage(pdfdoc, "Error parsing batch file: " + e.getMessage());
            throw new ParseException(e.getMessage());
        }

        //  make sure we got the type we expected, then cast it
        if (!(parsedObject instanceof CustomerInvoiceWriteoffBatchVO)) {
            LOG.error("Parsed file was not of the expected type.  Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "].");
            writeInvoiceSectionMessage(pdfdoc, "Parsed file was not of the expected type.  Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "].");
            throw new RuntimeException("Parsed file was not of the expected type.  Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "].");
        }

        //  convert to the real object type
        CustomerInvoiceWriteoffBatchVO batchVO = (CustomerInvoiceWriteoffBatchVO) parsedObject;

        LOG.info("Beginning validation and preparation of batch file.");
        createCustomerInvoiceWriteoffDocumentsFromBatchVO(batchVO, pdfdoc);

        return result;
    }

    /**
     * @see org.kuali.kfs.module.ar.document.service.CustomerInvoiceWriteoffDocumentService#createCustomerInvoiceWriteoffDocumentsFromBatchVO(org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO)
     */
    protected void createCustomerInvoiceWriteoffDocumentsFromBatchVO(CustomerInvoiceWriteoffBatchVO batchVO, com.lowagie.text.Document pdfdoc) {

        //  retrieve the Person from the batch
        Entity entity = KimApiServiceLocator.getIdentityService().getEntityByPrincipalName(batchVO.getSubmittedByPrincipalName());
        if (entity == null) {
            throw new RuntimeException("The Person who initiated this batch could not be retrieved.");
        }

        String createdOn = batchVO.getSubmittedOn();

        //  retrieve the user note
        String note = batchVO.getNote();

        //  add submittedOn and submittedBy to the pdf
        writeInvoiceSectionMessage(pdfdoc, "Batch Submitted By: " + batchVO.getSubmittedByPrincipalName());
        writeInvoiceSectionMessage(pdfdoc, "Batch Submitted On: " + batchVO.getSubmittedOn());
        if (StringUtils.isNotBlank(note)) {
            writeInvoiceSectionMessage(pdfdoc, "NOTE: " + note);
        }

        //  create a new Invoice Writeoff document for each invoice number in the batch file
        boolean succeeded = true;
        boolean customerNoteIsSet = false;
        String writeoffDocNumber = null;
        for (String invoiceNumber : batchVO.getInvoiceNumbers()) {

            //  set the customer note
            if (!customerNoteIsSet) {
                Customer customer = invoiceDocumentService.getCustomerByInvoiceDocumentNumber(invoiceNumber);
                if (customer != null) {
                    customerService.createCustomerNote(customer.getCustomerNumber(), note);
                    customerNoteIsSet = true;
                }
            }

            //  write the doc # we're trying to write off
            writeInvoiceSectionTitle(pdfdoc, "INVOICE DOC#: " + invoiceNumber);

            //  attempt to create the writeoff document
            succeeded = true;
            writeoffDocNumber = null;
            try {
                writeoffDocNumber = getInvoiceWriteoffDocumentService().createCustomerInvoiceWriteoffDocument(invoiceNumber, note);
            } catch (WorkflowException e) {
                succeeded = false;
                writeInvoiceSectionMessage(pdfdoc, "ERROR - Failed to create and route the Invoice Writeoff Document.");
                writeInvoiceSectionMessage(pdfdoc, "EXCEPTION DETAILS: " + e.getMessage());
            }

            //  write the successful information if we got it
            if (succeeded) {
                if (StringUtils.isNotBlank(writeoffDocNumber)) {
                    writeInvoiceSectionMessage(pdfdoc, "SUCCESS - Created new Invoice Writeoff Document #" + writeoffDocNumber);
                } else {
                    writeInvoiceSectionMessage(pdfdoc, "FAILURE - No error occurred, but a new Invoice Writeoff Document number was not created.  Check the logs.");
                }
            }
        }
    }

    /**
     * Accepts a file name and returns a byte-array of the file name contents, if possible.
     * <p>
     * Throws RuntimeExceptions if FileNotFound or IOExceptions occur.
     *
     * @param fileName String containing valid path & filename (relative or absolute) of file to load.
     * @return A Byte Array of the contents of the file.
     */
    protected byte[] safelyLoadFileBytes(String fileName) {

        InputStream fileContents;
        byte[] fileByteContent;
        try {
            fileContents = new FileInputStream(fileName);
        } catch (FileNotFoundException e1) {
            LOG.error("Batch file not found [" + fileName + "]. " + e1.getMessage());
            throw new RuntimeException("Batch File not found [" + fileName + "]. " + e1.getMessage());
        }
        try {
            fileByteContent = IOUtils.toByteArray(fileContents);
        } catch (IOException e1) {
            LOG.error("IO Exception loading: [" + fileName + "]. " + e1.getMessage());
            throw new RuntimeException("IO Exception loading: [" + fileName + "]. " + e1.getMessage());
        }
        return fileByteContent;
    }

    protected List<String> getListOfFilesToProcess() {

        //  create a list of the files to process
        List<String> fileNamesToLoad = batchInputFileService.listInputFileNamesWithDoneFile(batchInputFileType);

        if (fileNamesToLoad == null) {
            LOG.error("BatchInputFileService.listInputFileNamesWithDoneFile(" +
                batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen.");
            throw new RuntimeException("BatchInputFileService.listInputFileNamesWithDoneFile(" +
                batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen.");
        }

        //  filenames returned should never be blank/empty/null
        for (String inputFileName : fileNamesToLoad) {
            if (StringUtils.isBlank(inputFileName)) {
                LOG.error("One of the file names returned as ready to process [" + inputFileName +
                    "] was blank.  This should not happen, so throwing an error to investigate.");
                throw new RuntimeException("One of the file names returned as ready to process [" + inputFileName +
                    "] was blank.  This should not happen, so throwing an error to investigate.");
            }
        }

        return fileNamesToLoad;
    }

    protected com.lowagie.text.Document getPdfDoc() throws IOException, DocumentException {

        String reportDropFolder = reportsDirectory + "/" + ArConstants.CustomerInvoiceWriteoff.CUSTOMER_INVOICE_WRITEOFF_REPORT_SUBFOLDER + "/";
        String fileName = ArConstants.CustomerInvoiceWriteoff.BATCH_REPORT_BASENAME + "_" +
            new SimpleDateFormat("yyyyMMdd_HHmmssSSS").format(dateTimeService.getCurrentDate()) + ".pdf";

        //  setup the writer
        File reportFile = new File(reportDropFolder + fileName);
        FileOutputStream fileOutStream;
        fileOutStream = new FileOutputStream(reportFile);
        BufferedOutputStream buffOutStream = new BufferedOutputStream(fileOutStream);

        com.lowagie.text.Document pdfdoc = new com.lowagie.text.Document(PageSize.LETTER, 54, 54, 72, 72);
        PdfWriter.getInstance(pdfdoc, buffOutStream);

        pdfdoc.open();

        return pdfdoc;
    }

    protected void writeFileNameSectionTitle(com.lowagie.text.Document pdfDoc, String filenameLine) {
        Font font = FontFactory.getFont(FontFactory.COURIER, 10, Font.BOLD);

        //  file name title, get title only, on windows & unix platforms
        String fileNameOnly = filenameLine.toUpperCase();
        int indexOfSlashes = fileNameOnly.lastIndexOf("\\");
        if (indexOfSlashes < fileNameOnly.length()) {
            fileNameOnly = fileNameOnly.substring(indexOfSlashes + 1);
        }
        indexOfSlashes = fileNameOnly.lastIndexOf("/");
        if (indexOfSlashes < fileNameOnly.length()) {
            fileNameOnly = fileNameOnly.substring(indexOfSlashes + 1);
        }

        Paragraph paragraph = new Paragraph();
        paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT);
        Chunk chunk = new Chunk(fileNameOnly, font);
        chunk.setBackground(Color.LIGHT_GRAY, 5, 5, 5, 5);
        paragraph.add(chunk);

        //  blank line
        paragraph.add(new Chunk("", font));

        try {
            pdfDoc.add(paragraph);
        } catch (DocumentException e) {
            LOG.error("iText DocumentException thrown when trying to write content.", e);
            throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
        }
    }

    protected void writeInvoiceSectionTitle(com.lowagie.text.Document pdfDoc, String customerNameLine) {
        Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.BOLD + Font.UNDERLINE);

        Paragraph paragraph = new Paragraph();
        paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT);
        paragraph.add(new Chunk(customerNameLine, font));

        //  blank line
        paragraph.add(new Chunk("", font));

        try {
            pdfDoc.add(paragraph);
        } catch (DocumentException e) {
            LOG.error("iText DocumentException thrown when trying to write content.", e);
            throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
        }
    }

    protected void writeInvoiceSectionMessage(com.lowagie.text.Document pdfDoc, String resultLine) {
        Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.NORMAL);

        Paragraph paragraph = new Paragraph();
        paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT);
        paragraph.add(new Chunk(resultLine, font));

        //  blank line
        paragraph.add(new Chunk("", font));

        try {
            pdfDoc.add(paragraph);
        } catch (DocumentException e) {
            LOG.error("iText DocumentException thrown when trying to write content.", e);
            throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
        }
    }

    /**
     * @see org.kuali.kfs.module.ar.batch.service.CustomerInvoiceWriteoffBatchService#createBatchDrop(org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO)
     */
    @Override
    public String createBatchDrop(Person person, CustomerInvoiceWriteoffBatchVO writeoffBatchVO) {

        org.w3c.dom.Document xmldoc = transformVOtoXml(writeoffBatchVO);

        String batchXmlFileName = dropXmlFile(person, xmldoc);

        createDoneFile(batchXmlFileName);

        return batchXmlFileName;
    }

    protected String getBatchXMLNamespace() {
        return XML_BATCH_NAMESPACE;
    }

    protected String doneFileName(String filename) {
        String fileNoExtension = filename.substring(0, filename.lastIndexOf("."));
        return fileNoExtension + ".done";
    }

    protected void createDoneFile(String filename) {
        String fileNoExtension = doneFileName(filename);
        File doneFile = new File(fileNoExtension);
        try {
            doneFile.createNewFile();
        } catch (IOException e) {
            throw new RuntimeException("Exception while trying to create .done file.", e);
        }
    }

    protected String getBatchFilePathAndName(Person person) {

        String filename = batchInputFileType.getFileName(person.getPrincipalId(), "", "");

        String filepath = batchInputFileType.getDirectoryPath();
        if (!filepath.endsWith("/")) {
            filepath = filepath + "/";
        }

        String extension = batchInputFileType.getFileExtension();

        return filepath + filename + "." + extension;
    }

    protected String dropXmlFile(Person person, org.w3c.dom.Document xmldoc) {

        //  determine file paths and names
        String filename = getBatchFilePathAndName(person);

        //  setup the file stream
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(filename);
            try {
                //  setup the output format
                OutputFormat of = new OutputFormat("XML", "UTF-8", true);
                of.setIndent(1);
                of.setIndenting(true);

                //  setup the xml serializer and do the serialization
                Element docElement = xmldoc.getDocumentElement();
                XMLSerializer serializer = new XMLSerializer(fos, of);
                serializer.asDOMSerializer();
                serializer.serialize(docElement);
            } finally {
                // close the output stream
                if (fos != null) {
                    fos.close();
                }
            }
        } catch (IOException e) {
            throw new RuntimeException("Exception while writing customer invoice writeoff xml file.", e);
        }

        return filename;
    }

    protected Document transformVOtoXml(CustomerInvoiceWriteoffBatchVO writeoffBatchVO) {

        Document xmldoc = new DocumentImpl();
        Element e = null;
        Element invoicesElement = null;
        Node n = null;

        Element root = xmldoc.createElementNS("http://www.kuali.org/kfs/ar/customer", XML_ROOT_ELEMENT_NAME);
        root.setAttribute("xmlns", getBatchXMLNamespace());
        root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");

        //  create submittedBy element
        e = xmldoc.createElement(ArPropertyConstants.SUBMITTED_BY_PRINCIPAL_ID);
        n = xmldoc.createCDATASection(writeoffBatchVO.getSubmittedByPrincipalName());
        e.appendChild(n);
        root.appendChild(e);

        //  create submittedOn element
        e = xmldoc.createElement(ArPropertyConstants.SUBMITTED_ON);
        n = xmldoc.createCDATASection(writeoffBatchVO.getSubmittedOn());
        e.appendChild(n);
        root.appendChild(e);

        //  create note element
        e = xmldoc.createElement(ArPropertyConstants.NOTE);
        n = xmldoc.createCDATASection(writeoffBatchVO.getNote());
        e.appendChild(n);
        root.appendChild(e);

        //  create invoices element and list of invoice child elements
        invoicesElement = xmldoc.createElement(ArConstants.LOOKUP_INVOICE_NUMBERS);
        for (String invoiceNumber : writeoffBatchVO.getInvoiceNumbers()) {
            e = xmldoc.createElement(ArConstants.LOOKUP_INVOICE_NUMBER);
            n = xmldoc.createCDATASection(invoiceNumber);
            e.appendChild(n);
            invoicesElement.appendChild(e);
        }
        root.appendChild(invoicesElement);

        xmldoc.appendChild(root);

        return xmldoc;
    }

    // this strange construct (rather than using setter injection) is here to eliminate a
    // circular reference problem with Spring's eager init.
    protected CustomerInvoiceWriteoffDocumentService getInvoiceWriteoffDocumentService() {
        return SpringContext.getBean(CustomerInvoiceWriteoffDocumentService.class);
    }

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

    public void setBatchInputFileService(BatchInputFileService batchInputFileService) {
        this.batchInputFileService = batchInputFileService;
    }

    public void setBatchInputFileType(BatchInputFileType batchInputFileType) {
        this.batchInputFileType = batchInputFileType;
    }

    public void setReportsDirectory(String reportsDirectory) {
        this.reportsDirectory = reportsDirectory;
    }


    public void setCustomerService(CustomerService customerService) {
        this.customerService = customerService;
    }

    public void setInvoiceDocumentService(CustomerInvoiceDocumentService invoiceDocumentService) {
        this.invoiceDocumentService = invoiceDocumentService;
    }

}
