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.List;
import java.util.Map;
import java.util.logging.Logger;

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;
import com.alexanderlogistics.mahnlauf.MahnlaufService;

import jakarta.inject.Inject;

/**
 * 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.
 * 
 * Changes 2.Feb. 2024
 * =====================
 * 
 * Es wurde festgestellt (und von Cargosoft bestätigt), dass eine DATEV Datei
 * aus Cargosoft (Ausgangsrechnungen) falsche Belegdaten bezüglch des
 * Belegdatums bereitstellt.
 * 
 * Es können in einer Datei Rechnungen aus unterschiedlichen Buchungsperioden
 * enthalten sein. So können in der Periode Dezember auch Belege aus Januar
 * enthalten sein. In diesem Fall wird bisher davon ausgegangen dass diese zum
 * angegebenen Wirtschafsjahr gehören. Das ist aber bei Cargosoft nicht der
 * Fall. Vielmehr müssen die Belegdaten für das Folgejahr angelegt weden.
 * 
 * Beispiel:
 * 
 * Wirtschafts Jahr = 20230101
 * 
 * Buchunsbeginn = 20231201
 * 
 * Buchungsende = 20231231
 * 
 * Als Belegdatum wird nun der 1601 angegeben. Da der Beleg-Monat vor dem
 * angegebenen Buchungszeitraum liegt (Monat-Beleg < Monat-Stapel) muss künftig
 * das Beleg Jahr um 1 erhöht werden. Diese Regel gilt nicht wenn in der Datei
 * Belege liegen deren Beleg-Monat größer als der Stapel-Monat sind. Also in
 * einer Datei für August liegen Beleg-Monate aus September. Dann bleibt das
 * Beleg-Jahr wie bisher.
 * 
 * Der Fall tritt scheinbar also nur im Januar mit Belegstapeln aus Dezember des
 * Vorjahres auf .
 * 
 * Als Lösung wird daher hier die Kopfzeile nochnmal ausgelesen und der Monat
 * aus dem Buchungstapel beginn genommen (z.b. 12). Liegt nun der Monat aus dem
 * Belegdatum vor diesem Datum (z.b. 01) wir das Jahr um 1 erhöht. Das
 * Wirtschaftsjahr wird ignoriert.
 *
 * 4.4.2024
 * ----
 * Der Adapter prüft nun optional die Konfigurationsparameter
 * - model
 * - task
 * - event
 * 
 * <datev-cargosoft-import name=
 * "model-invoice">rechnungsausgang-dwc-1.0</datev-cargosoft-import>
 * <datev-cargosoft-import name="task-invoice">1000</datev-cargosoft-import>
 * <datev-cargosoft-import name="event-invoice">100</datev-cargosoft-import>
 * <datev-cargosoft-import name=
 * "model-tax">rechnungsausgang-dwc-1.0</datev-cargosoft-import>
 * <datev-cargosoft-import name="task-tax">1000</datev-cargosoft-import>
 * <datev-cargosoft-import name="event-tax">100</datev-cargosoft-import>
 * 
 * @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] .*$";
    public static final String CONFIG_ERROR = "CONFIG_ERROR";

    @Inject
    WorkflowService workflowService;

    @Inject
    DocumentService documentService;

    @Inject
    KreditorDebitorService kreditorDebitorService;

    @Inject
    TeamService teamService;

    @Inject
    MahnlaufService mahnlaufService;

    private String INITIAL_MODEL_VERSION_INVOICE;
    private int INITIAL_TASK_ID_INVOICE;
    private int INITIAL_EVENT_ID_INVOICE;

    /**
     * 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...");
        // Set default values
        INITIAL_MODEL_VERSION_INVOICE = "rechnungsausgang-de-1.0";
        INITIAL_TASK_ID_INVOICE = 5001;
        INITIAL_EVENT_ID_INVOICE = 980;

        // Try to read the optional cargosoft import options...
        ItemCollection evalItemCollection = workflowService.evalWorkflowResult(event, "datev-cargosoft-import",
                workitem, false);
        if (evalItemCollection != null) {
            // Ausgangsrechnung
            INITIAL_MODEL_VERSION_INVOICE = evalItemCollection.getItemValueString("model-invoice");
            if (INITIAL_MODEL_VERSION_INVOICE.isEmpty()) {
                INITIAL_MODEL_VERSION_INVOICE = "rechnungsausgang-de-1.0"; // set default
            }
            INITIAL_TASK_ID_INVOICE = evalItemCollection.getItemValueInteger("task-invoice");
            if (INITIAL_TASK_ID_INVOICE == 0) {
                INITIAL_TASK_ID_INVOICE = 5001; // set default
            }
            INITIAL_EVENT_ID_INVOICE = evalItemCollection.getItemValueInteger("event-invoice");
            if (INITIAL_EVENT_ID_INVOICE == 0) {
                INITIAL_EVENT_ID_INVOICE = 980; // set default
            }
        }
        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 = mahnlaufService.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));

            // Wir lesen hier nochmal das Datum des Buchunsstpels aus und berechnen den
            // Monat des Buchungsstapels.
            // dieser Monat wird benötigt um ggf. das Buchungsjahr zu ändern (Das ist eine
            // Besonderheit von cargosoft)
            String header1;
            header1 = in.readLine();
            String[] header1List = header1.split(";(?=([^\"]*\"[^\"]*\")*[^\"]*$)", 99);
            workitem.setItemValue("datev.Datum-von", DatevImportAdapter.csvVal(header1List[14]));
            workitem.setItemValue("datev.Datum-bis", DatevImportAdapter.csvVal(header1List[15]));
            // Monat des Buchungsstapels berechnen
            String stapelDatumBeginn = workitem.getItemValueString("datev.Datum-von");
            String stapelMonat = stapelDatumBeginn.substring(4, 6);

            // skip second header line
            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[] dataList = dataLine.split(";(?=([^\"]*\"[^\"]*\")*[^\"]*$)", 99);
                if (dataList.length < 6) {
                    throw new PluginException(DatevCargosoftImportAdapter.class.getName(),
                            DatevImportAdapter.DATEV_IMPORT_ERROR, "Invalid data in line " + line);
                }

                String belegNummer = DatevImportAdapter.csvVal(dataList[10]);
                String gegenKonto = DatevImportAdapter.csvVal(dataList[7]);
                String text = DatevImportAdapter.csvVal(dataList[13]);
                String buchungsTag = DatevImportAdapter.csvVal(dataList[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 = stapelDatumBeginn.substring(0, 4);

                // Cargosoft Spezial Lösung.
                // Liegt der Beleg-Monat vor dem stapelMonat (z.b. Rechnung aus Januar in einer
                // Dezember Datei), dann erhöhen wir das Beleg-datum-Jahr. Hier wird also nicht
                // wie normalerweise das Wirtschafsjahr betrchtet.
                if (Integer.parseInt(stapelMonat) > Integer.parseInt(monat)) {
                    int _year = Integer.parseInt(jahr) + 1;
                    jahr = _year + "";
                }
                Date dateBuchung = dateFormatter.parse(jahr + monat + tag);
                // Fälligkeit
                Date dueDate = new Date();
                try {
                    dueDate = dateFormatter.parse(DatevImportAdapter.csvVal(dataList[23]));
                } catch (Exception e) {
                    dueDate = new Date();
                }

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

                // kurse
                double kurs = parseBetrag(DatevImportAdapter.csvVal(dataList[3]));
                double basisumsatz = parseBetrag(DatevImportAdapter.csvVal(dataList[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(dataList[0])));
                dataItemCol.setItemValue("datev.shzeichen", DatevImportAdapter.csvVal(dataList[1]));
                dataItemCol.setItemValue("datev.wkz", waehrung);
                dataItemCol.setItemValue("datev.kurs", parseBetrag(DatevImportAdapter.csvVal(dataList[3])));
                dataItemCol.setItemValue("datev.basisumsatz", parseBetrag(DatevImportAdapter.csvVal(dataList[4])));
                dataItemCol.setItemValue("datev.konto", DatevImportAdapter.csvVal(dataList[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
                    closeDatevWorkitem(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
            closeDatevWorkitem(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;

    }

    /**
     * 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(), "DUPLICATE",
                        "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 closeDatevWorkitem(ItemCollection datevWorkitem, List<ItemCollection> splitBuchungen,
            Map<String, List<String>> spacePosMappings)
            throws AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
        if (datevWorkitem == 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);
        datevWorkitem.setItemValue("invoice.total", total);
        datevWorkitem.setItemValue("invoice.saldo", total);
        // implode die Splitbuchungen
        InvoiceUtil.implodeChildList(datevWorkitem, splitBuchungen);

        // set dbtr.name
        ItemCollection dbtrItemCol = kreditorDebitorService
                .findDebitor(datevWorkitem.getItemValueString("dbtr.number"));
        if (dbtrItemCol != null) {
            datevWorkitem.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");
            datevWorkitem.setItemValue("dbtr.country", country);
            if (country.equalsIgnoreCase("de") || country.equalsIgnoreCase("ch") || country.equalsIgnoreCase("at")) {
                datevWorkitem.setItemValue("invoice.language", "DE");
            } else {
                datevWorkitem.setItemValue("invoice.language", "EN");
            }
        }

        // Jezt noch das Space mapping
        mahnlaufService.mapInvoiceTextToSpace(datevWorkitem, spacePosMappings);
        // // dazu kucken wir in alle spaces den config wert 'pos.mapping'
        // String text = datevWorkitem.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
        // datevWorkitem.setItemValue("space.ref", entry.getKey());
        // found = true;
        // break;
        // }
        // }
        // if (found) {
        // break;
        // }
        // }
        // }

        // Processe die Invoice
        datevWorkitem.model(INITIAL_MODEL_VERSION_INVOICE).task(INITIAL_TASK_ID_INVOICE)
                .event(INITIAL_EVENT_ID_INVOICE);
        workflowService.processWorkItem(datevWorkitem);

    }

}