package com.alexanderlogistics.datev;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;

import org.imixs.marty.team.TeamService;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.SignalAdapter;
import org.imixs.workflow.datev.imports.DatevImportAdapter;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.exceptions.AccessDeniedException;
import org.imixs.workflow.exceptions.AdapterException;
import org.imixs.workflow.exceptions.ModelException;
import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.ProcessingErrorException;
import org.imixs.workflow.exceptions.QueryException;

import com.alexanderlogistics.InvoiceUtil;
import com.alexanderlogistics.KreditorDebitorService;

/**
 * Der DatevCargosoftImportAdapter liest eine von Cargosoft bereitgestellte
 * DATEV Datei ein und erzeugt Ausgangsrechnungs Workitems. Die Cargosoft DATEV
 * Datei enthält auch Eingangsrechnugen, welche von diesem Adapter ignoriert
 * werden.
 * <p>
 * In der Spalte Belegart-4 steht das Fäligkeitsdatum.
 * <p>
 * Beim Import wird auch der Kreditor geprüft und aus diesem das
 * Länderkennzeichen übernommen.
 * <p>
 * Zusätzlich wird anhand der 1. Positionsnummer eine Zuordnung zu einem Bereich
 * gemacht. Dazu werden aus den Bereichen die Konfigurations Items 'pos.mapping'
 * ausgelesen, welche Regular Expressions enthalten können. Gibt es einen match,
 * wird der Space der Rechnung zugeordnet.
 * 
 * @version 1.0
 * @author rsoika
 */
public class DatevCargosoftImportAdapter implements SignalAdapter {

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

    public static final String ENCODING = "ISO-8859-1";
    public static final String CHILD_ITEM_PROPERTY = "_ChildItems";
    public static final String REGEX_IMPORTTEXTPATTERN = "^R[1-8] .*$";

    @Inject
    WorkflowService workflowService;

    @Inject
    DocumentService documentService;

    @Inject
    KreditorDebitorService kreditorDebitorService;

    @Inject
    TeamService teamService;

    /**
     * This method finds or create the Datev Export and adds a reference
     * ($workitemref) to the current invoice.
     * 
     * @throws PluginException
     */
    @Override
    public ItemCollection execute(ItemCollection workitem, ItemCollection event)
            throws AdapterException, PluginException {

        logger.info("...parse datev file import...");
        try {

            // Datei einlesen
            List<String> log = readData(workitem);
            workitem.setItemValue("import.log", log);
        } catch (ModelException e) {
            throw new AdapterException(PluginException.class.getSimpleName(), DatevImportAdapter.DATEV_IMPORT_ERROR,
                    "Failed to import datev data!", e);

        }
        return workitem;
    }

    /**
     * Liest die datenzeilen ab Zeile 3 aus und bildet worktiems gruppiert nach
     * belegnummer
     * 
     * <p>
     * Belegfeld-1 ist hier ein eindeutiger Schlüssel für die Erzeugung von
     * Splitbuchungen
     * 
     * @param document
     * @throws PluginException
     * @throws ModelException
     * @throws ProcessingErrorException
     * @throws AccessDeniedException
     */
    private List<String> readData(ItemCollection workitem)
            throws PluginException, AccessDeniedException, ProcessingErrorException, ModelException {
        List<String> logBuffer = new ArrayList<String>();
        int line = 0;
        int positions = 0;
        int invoices = 0;
        int dupplicates = 0;
        int rueckstellungen = 0;
        int eingangsrechnungen = 0;

        String dataLine;
        // Start Datum Wirtschaftsjahr berechnen (JJJJMMTT)
        String wjBeginn = workitem.getItemValueString("datev.WJ-Beginn");
        String wjBeginnMonat = wjBeginn.substring(4, 6);
        SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd");

        // load spaces Pos Expressions
         Map<String, List<String>> spacePosMappings = loadSpacePosMappings();
       

        ItemCollection invoiceWorkitem = null;
        List<ItemCollection> splitBuchungen = new ArrayList<ItemCollection>();

        FileData fileData = workitem.getFileData().get(0);
        ByteArrayInputStream imputStream = new ByteArrayInputStream(fileData.getContent());

        log(logBuffer, "Importdatei: " + fileData.getName());
        try {
            BufferedReader in = new BufferedReader(new InputStreamReader(imputStream, ENCODING));

            // skip header
            in.readLine();
            in.readLine();

            // read content....
            line = 0;
            int blockSize = 0;
            while ((dataLine = in.readLine()) != null) {
                line++;
                if (blockSize >= 100) {
                    blockSize = 0;
                    logger.info(line + " entries read....");
                }

                // alle werte parsen
                String[] header1List = dataLine.split(";(?=([^\"]*\"[^\"]*\")*[^\"]*$)", 99);
                if (header1List.length < 6) {
                    throw new PluginException(DatevCargosoftImportAdapter.class.getName(),
                            DatevImportAdapter.DATEV_IMPORT_ERROR, "Invalid data in line " + line);
                }

                String belegNummer = DatevImportAdapter.csvVal(header1List[10]);
                String gegenKonto = DatevImportAdapter.csvVal(header1List[7]);
                String text = DatevImportAdapter.csvVal(header1List[13]);
                String buchungsTag = DatevImportAdapter.csvVal(header1List[9]);
                // cargosoft laesst gerne die führende 0 weg....
                if (buchungsTag.length() == 3) {
                    buchungsTag = "0" + buchungsTag;
                }
                // berechne Buchungsdatum DDMM
                String tag = buchungsTag.substring(0, 2);
                String monat = buchungsTag.substring(2, 4);
                String jahr = wjBeginn.substring(0, 4);
                // compute next year if start of WJ is not January
                if (Integer.parseInt(wjBeginnMonat) > Integer.parseInt(monat)) {
                    jahr = Integer.parseInt(jahr + 1) + "";
                }
                Date dateBuchung = dateFormatter.parse(jahr + monat + tag);
                // Fälligkeit
                Date dueDate = new Date();
                try {
                    dueDate=dateFormatter.parse(DatevImportAdapter.csvVal(header1List[23]));
                } catch (Exception e) {
                    dueDate = new Date();
                }

                String paymentTerm = DatevImportAdapter.csvVal(header1List[31]);
                String waehrung = DatevImportAdapter.csvVal(header1List[2]);

                // kurse
                double kurs = parseBetrag(DatevImportAdapter.csvVal(header1List[3]));
                double basisumsatz = parseBetrag(DatevImportAdapter.csvVal(header1List[4]));

                // Skip Eingangsrechnungen
                if (!gegenKonto.startsWith("1")) {
                    eingangsrechnungen++;
                    // skip
                    continue;
                }

                // Skip Rückstellung
                // text darf nicht mit R1 bis R8 beginnen
                // ^R[1-8] .
                if (text.matches(REGEX_IMPORTTEXTPATTERN)) {
                    rueckstellungen++;
                    // skip
                    continue;
                }

                positions++;
                logger.info("...read dbtr: " + gegenKonto + " beleg-nr: " + belegNummer);

                // Erzeuge einen Buchungssatz
                ItemCollection dataItemCol = new ItemCollection();
                dataItemCol.setItemValue("datev.text", text);
                dataItemCol.setItemValue("datev.umsatz", parseBetrag(DatevImportAdapter.csvVal(header1List[0])));
                dataItemCol.setItemValue("datev.shzeichen", DatevImportAdapter.csvVal(header1List[1]));
                dataItemCol.setItemValue("datev.wkz", waehrung);
                dataItemCol.setItemValue("datev.kurs", parseBetrag(DatevImportAdapter.csvVal(header1List[3])));
                dataItemCol.setItemValue("datev.basisumsatz", parseBetrag(DatevImportAdapter.csvVal(header1List[4])));
                dataItemCol.setItemValue("datev.konto", DatevImportAdapter.csvVal(header1List[6]));

                // Haben wir schon eine Invoice Workitem angelegt?
                if (invoiceWorkitem == null) {
                    logger.info("...create new workitem....");
                    // erzeuge ein neues
                    try {
                        invoiceWorkitem = createEmptyInvoice(belegNummer, text, gegenKonto, dateBuchung, dueDate,
                                paymentTerm, waehrung, kurs, basisumsatz);
                        invoices++;
                    } catch (AdapterException e) {
                        dupplicates++;
                        continue;
                    }
                }

                // ist es noch die selbe Belegnummer?
                if (belegNummer.equals(invoiceWorkitem.getItemValueString("invoice.number"))) {
                    // adde die Splitbuchung
                    splitBuchungen.add(dataItemCol);
                    logger.info("...add splitbuchung.2...");
                } else {
                    logger.info("...close workitem....");

                    // nein - also invoice schließen
                    closeInvoiceWorkitem(invoiceWorkitem, splitBuchungen,spacePosMappings);
                    invoiceWorkitem = null;
                    // neue Splitbuchungstabelle anlegen
                    splitBuchungen = new ArrayList<ItemCollection>();
                    // invoice zurücksetzten
                    // erzeuge ein neues
                    try {
                        invoiceWorkitem = createEmptyInvoice(belegNummer, text, gegenKonto, dateBuchung, dueDate,
                                paymentTerm, waehrung, kurs, basisumsatz);
                        invoices++;
                    } catch (AdapterException e) {
                        dupplicates++;
                        continue;
                    }
                    splitBuchungen.add(dataItemCol);
                }

            } // wile end
            closeInvoiceWorkitem(invoiceWorkitem, splitBuchungen,spacePosMappings);

        } catch (IOException | ParseException e) {
            throw new PluginException(DatevCargosoftImportAdapter.class.getName(),
                    DatevImportAdapter.DATEV_IMPORT_ERROR, "Unable to read file", e);
        }

        log(logBuffer, line + " Zeilen eingelesen ");
        log(logBuffer, positions + " Rechnungspositionen eingelesen ");
        log(logBuffer, invoices + " Rechnungen importiert ");

        if (rueckstellungen > 0) {
            log(logBuffer, rueckstellungen + " Rückstellungen wurden übersprungen.");
        }
        if (eingangsrechnungen > 0) {
            log(logBuffer, eingangsrechnungen + " Eingangsrechnungen wurden übersprungen.");
        }

        if (dupplicates > 0) {
            log(logBuffer, dupplicates + " Rechnungen wurden übersprungen da diese bereits importiert wurden!");
        }

        // return log
        return logBuffer;

    }

    /**
     * Helper method that loads all expressions from the spaces
     * 
     * @return List of list of expressions
     */
    private Map<String,List<String>> loadSpacePosMappings() {
        Map<String,List<String>> result =new HashMap<>();
        // als erstes lesen wir die pos.mappings aus den Spaces ein, auf die dann eine
        // Rechnung emapped werden kann.
        List<ItemCollection> spaces_subs = teamService.getSpaces();
        for (ItemCollection stub: spaces_subs) {
            // load full workitem
            ItemCollection space = documentService.load(stub.getUniqueID());
            if (space!=null) {
                result.put(space.getUniqueID(),space.getItemValueList("pos.mapping", String.class));
            }
        }
            
        return result;
    }

    /**
     * This method returns a new empty invoice ItemCollection.
     * <p>
     * The method tests if an invoice with this number already exits. If so, the
     * method throws a AdapterException
     * 
     * @param belegNummer
     * @param text
     * @param gegenKonto
     * @param dateBuchung
     * @param dueDate
     * @param paymentTerm
     * @param currency
     * @param kurs
     * @param basisUmsatz
     * @return
     * @throws AdapterException
     */
    private ItemCollection createEmptyInvoice(String belegNummer, String text, String gegenKonto, Date dateBuchung,
            Date dueDate, String paymentTerm, String currency, double kurs, double basisUmsatz)
            throws AdapterException {

        // test if invoice was already imported before
        String sQuery = "((type:workitem OR type:workitemarchive) AND $modelversion:rechnungsausgang* AND invoice.number:\""
                + belegNummer + "\")";
        try {
            // find the textblock...
            List<ItemCollection> result = documentService.find(sQuery, 1, 0);
            if (result.size() > 0) {
                throw new AdapterException(DatevCargosoftImportAdapter.class.getName(), "DUPPLICATE",
                        "Invoice already imported");
            }
        } catch (QueryException e) {
            logger.warning("failed to search invoices by query: " + e.getMessage());
        }

        ItemCollection invoiceWorkitem = new ItemCollection();
        invoiceWorkitem.setItemValue("invoice.number", belegNummer);
        invoiceWorkitem.setItemValue("invoice.currency", currency);
        invoiceWorkitem.setItemValue("invoice.text", text);
        invoiceWorkitem.setItemValue("invoice.date", dateBuchung);
        invoiceWorkitem.setItemValue("dbtr.number", gegenKonto);
        invoiceWorkitem.setItemValue("invoice.duedate", dueDate);
        invoiceWorkitem.setItemValue("invoice.reminder", dueDate);
        invoiceWorkitem.setItemValue("payment.term", paymentTerm);

        invoiceWorkitem.setItemValue("invoice.rate", kurs);
        invoiceWorkitem.setItemValue("invoice.base.amount", basisUmsatz);

        return invoiceWorkitem;
    }

    private void log(List<String> log, String message) {
        log.add(new Date() + ": " + message + "\n");
    }

    /**
     * . entfernen , mit punkt ersetzten
     * 
     * @param value
     * @return
     */
    public Double parseBetrag(String value) {

        value = value.replace(".", "");
        value = value.replace(",", ".");

        return new Double(value);
    }

    /**
     * Diese Hilfsmethod schließt das Invoice Workitem ab
     * 
     * @throws ModelException
     * @throws PluginException
     * @throws ProcessingErrorException
     * @throws AccessDeniedException
     */
    private void closeInvoiceWorkitem(ItemCollection invoiceWorkitem, List<ItemCollection> splitBuchungen,
            Map<String, List<String>> spacePosMappings)
            throws AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
        if (invoiceWorkitem == null) {
            return; // no op
        }

        // berechne Total:
        double total = 0;
        for (ItemCollection zeile : splitBuchungen) {
            if ("H".equals(zeile.getItemValueString("datev.shzeichen"))) {
                total = total + zeile.getItemValueDouble("datev.umsatz");
            } else {
                total = total - zeile.getItemValueDouble("datev.umsatz");
            }
        }

        total=InvoiceUtil.round(total);
        invoiceWorkitem.setItemValue("invoice.total", total);
        invoiceWorkitem.setItemValue("invoice.saldo", total);
        // implode die Splitbuchungen
        implodeChildList(invoiceWorkitem, splitBuchungen);

        // set dbtr.name
        ItemCollection dbtrItemCol = kreditorDebitorService
                .findDebitor(invoiceWorkitem.getItemValueString("dbtr.number"));
        if (dbtrItemCol != null) {
            invoiceWorkitem.setItemValue("dbtr.name", dbtrItemCol.getItemValueString("_VENDOR_Name"));
            // Anhand der Creditoren Stammdaten errrechnen wir die Sprache für diese
            // Rechnung
            String country = dbtrItemCol.getItemValueString("_VENDOR_COUNTRY");
            invoiceWorkitem.setItemValue("dbtr.country", country);
            if (country.equalsIgnoreCase("de") || country.equalsIgnoreCase("ch") || country.equalsIgnoreCase("at")) {
                invoiceWorkitem.setItemValue("invoice.language", "DE");
            } else {
                invoiceWorkitem.setItemValue("invoice.language", "EN");
            }
        }

        // Jezt noch das Space mapping
        // dazu kucken wir in alle spaces den config wert 'pos.mapping'
        String text = invoiceWorkitem.getItemValueString("invoice.text");
        if (!text.isEmpty()) {
           boolean found=false;
            for (Map.Entry<String, List<String> > entry : spacePosMappings.entrySet()) {
                List<String> expressionsList=entry.getValue();
                for (String expr : expressionsList) {
                    
                    Pattern p = Pattern.compile(expr);
                    Matcher m = p.matcher(text);
                    if (m.find()) {
                        // we have a match
                        invoiceWorkitem.setItemValue("space.ref", entry.getKey());
                        found=true;
                        break;
                    }
                }
                if (found) {
                    break;
                }
            }
        }
        // Processe die Invoice
        invoiceWorkitem.model("rechnungsausgang-de-1.0").task(5001).event(980);
        workflowService.processWorkItem(invoiceWorkitem);

    }
 

    /**
     * Convert the List of ItemCollections back into a List of Map elements
     * 
     * @param workitem
     */
    @SuppressWarnings({ "rawtypes" })
    private void implodeChildList(ItemCollection workitem, List<ItemCollection> childItems) {
        List<Map> mapOrderItems = new ArrayList<Map>();
        // convert the child ItemCollection elements into a List of Map
        if (childItems != null) {
            logger.fine("Convert child items into Map...");
            // iterate over all order items..
            for (ItemCollection orderItem : childItems) {
                mapOrderItems.add(orderItem.getAllItems());
            }
            workitem.replaceItemValue(CHILD_ITEM_PROPERTY, mapOrderItems);
        }
    }

}