package com.alexanderlogistics;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.inject.Inject;

import org.apache.poi.ss.usermodel.CellCopyPolicy;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.imixs.archive.core.SnapshotService;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.exceptions.AdapterException;
import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.QueryException;
import org.imixs.workflow.poi.POIFindReplaceAdapter;

/**
 * Der OPListExportAdapter importiert eine Excel Datei aus einem Textblock
 * 
 * <pre>
 * {@code
        <opliste name="textblock">textblock-ref</opliste>
        <opliste name="template">filename</opliste>
        <opliste name="targetname">filename</opliste>
   }
 * </pre>
 * <p>
 * Der Adapter erweitert den POIAdapter somit können felder aktualisiert werden
 * (siehe POIFineReplaceAdapter).
 * 
 * <p>
 * Der Adapter kopiert die Rechnungsdaten in neue Zeilen, welche ab Zeilnenummer
 * 16 eingefügt werden.
 * <p>
 * 
 * Abschliessend wird die Zelle 'TOTAL' noch aktualisiert.
 * 
 * @version 1.0
 * @author rsoika
 */
public class OPListExportAdapter extends POIFindReplaceAdapter {

    private static Logger logger = Logger.getLogger(OPListExportAdapter.class.getName());

    public static final String ERROR_CONFIG = "CONFIG_ERROR";
    public static final int TASK_MAHNUNG_2ND = 5210;
    public static final int TASK_MAHNUNG_LAST = 5220;

    final String TYPE_TEXTBLOCK = "textblock";

    @Inject
    WorkflowService workflowService;

    @Inject
    DocumentService documentService;

    @Inject
    SnapshotService snapshotService;


    /**
     * This method finds or create the Zahlungsavis and adds a reference
     * ($workitemref) to the current invoice.
     * 
     * @throws PluginException
     */
    @SuppressWarnings("unchecked")
    @Override
    public ItemCollection execute(ItemCollection document, ItemCollection event)
            throws AdapterException, PluginException {

        // read the zahlungsavis options
        ItemCollection evalItemCollection = workflowService.evalWorkflowResult(event, "opliste", document, false);
        if (evalItemCollection == null) {
            throw new PluginException(OPListExportAdapter.class.getSimpleName(), ERROR_CONFIG,
                    "missing op-list configuration in model event - please check model configuration");
        }
        String textblock = evalItemCollection.getItemValueString("textblock");
        String template = evalItemCollection.getItemValueString("template");
        String targetName = evalItemCollection.getItemValueString("target-name");
        boolean filter = evalItemCollection.getItemValueBoolean("filter");
        // adapt text....
        targetName = workflowService.adaptText(targetName, document);

        appendExcelTemplate(document, textblock, template, targetName);

        logger.info("... loading poi configuration..");
        // Process POI instructions
        // read the config
        ItemCollection poiConfig = workflowService.evalWorkflowResult(event, "poi-update", document, false);
        if (poiConfig == null || !poiConfig.hasItem("findreplace")) {
            throw new PluginException(POIFindReplaceAdapter.class.getSimpleName(), CONFIG_ERROR,
                    "missing poi configuration");
        }
        List<String> replaceDevList = poiConfig.getItemValue("findreplace");
        String eval = poiConfig.getItemValueString("eval");
        try {
            logger.info("... update template with normal poi information..");
            this.updateFileData(document.getFileData(targetName), document, replaceDevList, eval);

            // now we add separate lines for each invoice....
            logger.info("... insert invoices - filter="+filter);
            insertInvoiceRows(document, targetName, filter);
        } catch (PluginException | IOException | QueryException e) {
            throw new PluginException(OPListExportAdapter.class.getSimpleName(), ERROR_CONFIG,
                    "failed to update op-liste: " + e.getMessage());
        }
        logger.info("... completed!");
        return document;
    }

    /**
     * This helper method inserts a row for each invoice of the OPListe at row 16
     * into the excel template file
     * <p>
     * The method copies the Row A16 as a reference row
     * <p>
     * The named cell 'TOTAL' should contain the summary formula. It will be
     * evaluated at the end.
     * 
     * @throws PluginException
     * @throws QueryException
     */
    private void insertInvoiceRows(ItemCollection document, String fileName, boolean filter)
            throws PluginException, QueryException {

        double saldoUSD = 0;
        double saldoEUR = 0;
        // load dummy rechnungen
        String spaceID = document.getItemValueString("space.ref");

        logger.info("... space.ref=" + spaceID + " ...grouping invoices by spaceid....");
        Map<String, List<ItemCollection>> invoiceMap = groupInvoicesBySpaceID(spaceID, filter);

        FileData fileData = document.getFileData(fileName);

        // load XSSFWorkbook
        XSSFWorkbook doc = null;
        try (InputStream imputStream = new ByteArrayInputStream(fileData.getContent())) {
            doc = new XSSFWorkbook(imputStream);
            // NOTE: we only take the first sheet !
            XSSFSheet sheet = doc.getSheetAt(0);
            CellReference categoryRefCell = new CellReference("A12");
            XSSFRow referenceRowCategory = sheet.getRow(categoryRefCell.getRow());
            CellReference invoiceRefCell = new CellReference("A13");
            XSSFRow referenceRowInvoice = sheet.getRow(invoiceRefCell.getRow());

            int referenceRowPos = 12;
            int rowPos = 12;
            int lastRow = 2999;
            logger.finest("Last rownum=" + lastRow);

            int totalRowCount = invoiceMap.size() * 2;
            for (List<?> list : invoiceMap.values()) {
                totalRowCount = totalRowCount + list.size();
            }
            if (totalRowCount == 0) {
                logger.warning("No Invoices found - skip update workbook!");
                return;
            }
            sheet.shiftRows(rowPos, lastRow, totalRowCount, true, true);
            for (Map.Entry<String, List<ItemCollection>> entry : invoiceMap.entrySet()) {
                String dbtrNumber = entry.getKey();
                List<ItemCollection> invoices = entry.getValue();

                // erzeuge eine Zwischenüberschrift für den Debitor....
                XSSFRow categoryRow = sheet.createRow(rowPos);
                categoryRow.copyRowFrom(referenceRowCategory, new CellCopyPolicy());
                // insert values
                categoryRow.getCell(0).setCellValue(dbtrNumber);
                String debitorName = invoices.get(0).getItemValueString("dbtr.name");
                categoryRow.getCell(1).setCellValue(debitorName);

                // calculate summ of all invoices
                double catSumEUR = 0;
                double catSumUSD = 0;
                for (ItemCollection invoice : invoices) {
                    if ("EUR".equals(invoice.getItemValueString("invoice.currency"))) {
                        catSumEUR = catSumEUR + invoice.getItemValueDouble("invoice.saldo");

                    } else {
                        catSumUSD = catSumUSD + invoice.getItemValueDouble("invoice.saldo");

                    }
                }

                categoryRow.getCell(6).setCellValue(catSumEUR);
                categoryRow.getCell(8).setCellValue(catSumUSD);

                rowPos++;

                // jetzt füge alle Rechnungen an.
                for (ItemCollection invoice : invoices) {
                    logger.fine("......add invoice " + invoice.getUniqueID());

                    // now create a new line..
                    XSSFRow row = sheet.createRow(rowPos);
                    row.copyRowFrom(referenceRowInvoice, new CellCopyPolicy());
                    // insert values
                    if (invoice.getItemValueBoolean("invoice.protest")) {
                        row.getCell(2).setCellValue(invoice.getItemValueString("invoice.number") + " (R)");
                    } else {
                        row.getCell(2).setCellValue(invoice.getItemValueString("invoice.number"));
                    }
                    row.getCell(3).setCellValue(invoice.getItemValueString("invoice.text"));
                    row.getCell(4).setCellValue(invoice.getItemValueDate("invoice.date"));
                    row.getCell(5).setCellValue(invoice.getItemValueDate("invoice.duedate"));
                    double total = invoice.getItemValueDouble("invoice.saldo");
                    String currency = invoice.getItemValueString("invoice.currency");
                    if ("EUR".equals(currency)) {
                        // EUR
                        row.getCell(6).setCellValue(total);
                        row.getCell(7).setCellValue(currency);
                        saldoEUR = InvoiceUtil.round(saldoEUR + total);
                    } else {
                        // USD
                        row.getCell(8).setCellValue(total);
                        row.getCell(9).setCellValue(currency);
                        saldoUSD = InvoiceUtil.round(saldoUSD + total);
                    }
                    row.getCell(10).setCellValue(invoice.getItemValueString("$workflowstatus"));

                    rowPos++;
                }

                // Leerzeile
                categoryRow = sheet.createRow(rowPos);
                rowPos++;
            }
            // delete reference row 12
            sheet.shiftRows(referenceRowPos, lastRow + totalRowCount, -1, true, true);

            // finally update the total formula...
            XSSFCell cTotalEUR = getCellByRef(doc, sheet, "TOTALEUR");
            XSSFCell cTotalUSD = getCellByRef(doc, sheet, "TOTALUSD");
            cTotalEUR.setCellValue(saldoEUR);
            cTotalUSD.setCellValue(saldoUSD);

            // write back the file
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            doc.write(byteArrayOutputStream);

            byte[] newContent = byteArrayOutputStream.toByteArray();
            FileData fileDataNew = new FileData(fileData.getName(), newContent, fileData.getContentType(), null);
            // update the fileData
            document.addFileData(fileDataNew);
            document.setItemValue("space.saldo.eur", saldoEUR);
            document.setItemValue("space.saldo.usd", saldoUSD);
            logger.finest("......new document added");

        } catch (IOException e) {
            throw new PluginException(OPListExportAdapter.class.getSimpleName(), ERROR_CONFIG,
                    "failed to update opliste: " + e.getMessage());
        } finally {
            if (doc != null) {
                try {
                    doc.close();
                } catch (IOException e) {
                    logger.severe("Failed to close workbook: " + e.getMessage());
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * This method loads a text-block for a specified ref and appends the named
     * fileData object of this document.
     * 
     * @param document
     * @throws PluginException
     */
    private void appendExcelTemplate(ItemCollection document, String textblockRef, String template, String targetName)
            throws PluginException {

        if ((template == null || template.isEmpty()) || (textblockRef == null || textblockRef.isEmpty())) {
            throw new PluginException(OPListExportAdapter.class.getSimpleName(), ERROR_CONFIG,
                    "invalid op-list configuration in model event - textblock/template reference not defined!");
        }

        // load the text block
        FileData fileData = loadTextBlockFileData(textblockRef, template);

        // do we found the document?
        if (fileData == null) {
            throw new PluginException(OPListExportAdapter.class.getSimpleName(), ERROR_CONFIG,
                    "invalid op-list configuration in model event - template=" + template + " not found!");
        }
        fileData.setName(targetName);
        // append document
        logger.info("...append new op-list Template: " + targetName);

        document.addFileData(fileData);

    }

    /**
     * This method returns a text-block ItemCollection for a specified name.
     * 
     * @param name in attribute txtname
     * 
     *
     */
    public FileData loadTextBlockFileData(String name, String fileName) {
        ItemCollection textBlockItemCollection = null;

        // load text-block by name....
        String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")";
        Collection<ItemCollection> col;
        try {
            // find the textblock...
            col = documentService.find(sQuery, 1, 0);
            if (col.size() > 0) {
                textBlockItemCollection = col.iterator().next();
                // fetch the fileData...
                return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName);

            } else {
                logger.warning("Missing text-block : '" + name + "'");
            }
        } catch (QueryException e) {
            logger.warning("getTextBlock - invalid query: " + e.getMessage());
        }

        return null;
    }

    /**
     * Diese Methode groupiert eine Rechnugnsliste nach Debitorennummern
     * 
     * Falls 'filter==true' werden nur Rechnungen ab der 2. Mahnung oder mit einer
     * Fälligkeit >21 Tage ausgegeben
     * 
     * @param spaceID
     * @return
     */
    private Map<String, List<ItemCollection>> groupInvoicesBySpaceID(String spaceID, boolean filter) {

        Map<String, List<ItemCollection>> result = new HashMap<>();

        try {
            List<ItemCollection> invoices = documentService.find(
                    "$modelversion:rechnungsausgang-de-1.0 AND type:workitem AND $uniqueidref:" + spaceID, 9999, 0,
                    "invoice.number", false);

            for (ItemCollection invoice : invoices) {

                String dbtrNumber = invoice.getItemValueString("dbtr.number");
                // haben wir shon ein listchen?
                List<ItemCollection> invoiceList = result.get(dbtrNumber);
                if (invoiceList == null) {
                    // erzeuge eine neue liste
                    invoiceList = new ArrayList<ItemCollection>();
                }

                if (filter) {
                    Date date = invoice.getItemValueDate("invoice.duedate");
                    if (OPListExportAdapter.isOverdue(date, invoice.getTaskID())) {
                        //logger.info("...invoice - "  +invoice.getUniqueID() + " is in overdue");
                        invoiceList.add(invoice);
                    }
                } else {
                    // jede Rechnung zählt (manuelle OPListe)
                    invoiceList.add(invoice);
                }

                // speicher wieder die neue liste wenn es mindestens eine Rechnung gab
                if (invoiceList.size()>0) {
                    result.put(dbtrNumber, invoiceList);
                }

            }

        } catch (QueryException e) {
            e.printStackTrace();
        }

        return result;
    }   

    public static boolean isOverdue(Date date, int taskid) {
        Date now = new Date();
        long diffInMillies = now.getTime() - date.getTime();
        int tage = (int) (diffInMillies / 1000 / 60 / 60 / 24);
        //logger.info("...taskid=" + taskid + " date=" + date + "  tage=" + tage);
        return ((taskid >= TASK_MAHNUNG_2ND && taskid <= TASK_MAHNUNG_LAST) || tage >= 21);

    }

}