package com.alexanderlogistics;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.plugins.AbstractPlugin;
import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.QueryException;

/**
 * Das InvoicePlugin prüft die Eingaben auf gültigkeit, so dass diese problemlos
 * nach Cargosoft expoertier werden können.
 * <p>
 * Konkret geht es darum, das im Status 5200 in den ChildItems keine leeren
 * Zeilen vorkommen dürfen.
 * <p>
 * Im Status 5000 muss immer der payment.type ausgewählt werden. Dies wird vom
 * Plugin explizit geprüft
 * <p>
 * Das Plugin prüft auch auf doppelte Rechnungsnummern. Dabei wird Systemweit
 * geprüft ob die Rechnungsnummer schon einmal vorkam. Falls ja, wird eine
 * Warnung ausgegeben. Der Benutzer kann diese dann skippen.
 * 
 * 
 * @author rsoika
 * @version 1.0
 * 
 */
public class InvoicePlugin extends AbstractPlugin {

    public static final int TASK_ERFASSUNG = 5001;
    public static final int TASK_SACHPRUEFUNG = 5200;
    public static final int EVENT_FREIGEBEN = 20;
    public static final String ITEM_INVOICE_PERIOD = "invoice.period";

    public static final String CHILD_ITEM_PROPERTY = "_ChildItems";
    public static final String ERROR_MISSING_DATA = "MISSING_DATA";
    public static final String ERROR_DUPPLICATE_INVOICE_NUMBER = "DUPPLICATE_INVOICE_NUMBER";

    // XX-YYY-####-###
    public static final String REGEX_POSNUMER = "([A-Z]{2}-[A-Z]{3}-[0-9]{4}-[0-9]{3})";

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

    /**
     * Test childworkitems for empty lines
     * 
     * @throws PluginException - if data is missing
     * 
     **/
    @Override
    public ItemCollection run(ItemCollection workitem, ItemCollection event) throws PluginException {

        // skip if validaten tag is required=false
        ItemCollection evalItemCollection = this.getWorkflowService().evalWorkflowResult(event, "validation", workitem);
        if (evalItemCollection != null) {
            // evaluate the validation rules...
            if ("false".equalsIgnoreCase(evalItemCollection.getItemValueString("required"))) {
                return workitem;
            }
        }

        boolean isPublicEvent = !("0".equals(event.getItemValueString("keypublicresult")));

        // Payment.type muss immer eingetragne werden!
        if (isPublicEvent && workitem.getTaskID() >= TASK_ERFASSUNG && workitem.getEventID() < 900) {
            if (workitem.getItemValueString("payment.type").trim().isEmpty()) {
                // throw a plugin exception!
                throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                        "Eingabefehler - Bitte wählen Sie eine Zahlungsart aus!");
            }
            // doppelte Rechnungsnummer prüfen
            if (workitem.getTaskID() == TASK_ERFASSUNG) {
                validateInvoiceNumber(workitem);
            }

            if (workitem.getTaskID() == TASK_ERFASSUNG || workitem.getTaskID() == TASK_SACHPRUEFUNG) {
                // Buchungsperiode auf plausi prüfen
                if ("workitem".equals(workitem.getType())) {
                    validateBuchungsperiode(workitem.getItemValueString(ITEM_INVOICE_PERIOD));
                }
                // Cargosoft Kreditorennnummer prüfen
                validateCargosoftCdtrNumber(workitem);
            }
        }

        // die prüfung der positionsnummern und Category erfolgt nur im Status
        // Sachprüfung (5200) und nur beim Freigeben (20)!
        if (workitem.getTaskID() == TASK_SACHPRUEFUNG && workitem.getEventID() == EVENT_FREIGEBEN) {
            List<ItemCollection> childs = explodeChildList(workitem);
            for (ItemCollection posItem : childs) {
                if (posItem.getItemValueString("name").trim().isEmpty()) {
                    // throw a plugin exception - because name is missing!
                    throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                            "Eingabefehler - Die Positionsnummer in Zeile " + posItem.getItemValueString("numpos")
                                    + " muss ausgefüllt sein!");
                }

                // validate pos for regex pattern 'XX-YYY-####-###'
                if (!posItem.getItemValueString("name").matches(REGEX_POSNUMER)) {
                    // throw a plugin exception - because name is missing!
                    throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                            "Eingabefehler - Die Positionsnummer in Zeile " + posItem.getItemValueString("numpos")
                                    + " muss im Format 'XX-YYY-####-###' eingegeben werden!");
                }

                if (posItem.getItemValueString("category").trim().isEmpty()) {
                    // throw a plugin exception - because name is missing!
                    throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                            "Eingabefehler - Die Leistungsart in Zeile " + posItem.getItemValueString("numpos")
                                    + " muss ausgefüllt sein!");
                }

                if (posItem.getItemValueString("tax").trim().isEmpty()) {
                    // throw a plugin exception - because name is missing!
                    throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                            "Eingabefehler - Die Steuer in Zeile " + posItem.getItemValueString("numpos")
                                    + " muss ausgefüllt sein!");
                }

                if (posItem.getItemValueFloat("amount") == 0) {
                    // throw a plugin exception - because name is missing!
                    throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                            "Eingabefehler - Der Nettobetrag in Zeile " + posItem.getItemValueString("numpos")
                                    + " darf nicht 0 sein!");
                }

                // Buchunsperiode
                if (!posItem.getItemValueString(ITEM_INVOICE_PERIOD).trim().isEmpty()) {
                    validateBuchungsperiode(posItem.getItemValueString(ITEM_INVOICE_PERIOD));

                }
            }

            // Ergaenzung 18.05.2021:
            // es kann vorkommen, das die Anwender abweichende Buchungsperioden eingeben so
            // das die Hauptbuchungsperiode gar ncht merh vorkommt. Das darf aber nicht der
            // fall sein.
            // Im folgenden überprüfen wir ob eine Buchungszeile vorkommt in der keine oder
            // die Hauptbuchunsperiode ausgewählt wurde. Ist das nicht der Fal gibt es eine
            // Fehlermeldung für den Anwendere
            if (childs.size() > 0) {
                String hauptBuchungsperiode = workitem.getItemValueString("invoice.period");
                boolean buchungsperiodenValid = false;
                for (ItemCollection posItem : childs) {
                    String posBuchungsperiode = posItem.getItemValueString("invoice.period");
                    if (posBuchungsperiode.isEmpty() || posBuchungsperiode.equals(hauptBuchungsperiode)) {
                        // alles fein!
                        buchungsperiodenValid = true;
                    }
                }
                if (buchungsperiodenValid == false) {
                    // fehlerhafte Buchungsperioden.
                    throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                            "Eingabefehler - Bitte überprüfen Sie die Hauptbuchungsperiode mit den Buchungsperioden der einzelnen Positionen!");
                }
            }

        }

        return workitem;
    }

    /**
     * Diese Method prüft die Buchungsperiode auf Plausibilität
     * 
     * YYYY(+1)01-12
     * 
     * z.b. 202110 oder 202107 oder 202201
     * 
     * @param workitem
     * @throws PluginException
     */
    private void validateBuchungsperiode(String period) throws PluginException {

        // buchungsperionde nur prüfen wenn noch nicht archiviert
        if (!period.isEmpty()) {
            LocalDate localDate = LocalDate.now();
            int year = localDate.getYear();
            // build regex....
            String regex = "(" + year + "|" + (year + 1) + ")(1[0-2]|0[1-9])";

            if (!period.matches(regex)) {
                // throw a plugin exception - because name is missing!
                throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                        "Eingabefehler - Bitte geben Sie ein gültige Buchungsperiode ein!");
            }
        }
    }

    /**
     * Diese Method prüft ob die "invoice.number" bereits einmal im
     * Rechnugnsworkflow vergeben wurde. Falls ja wird eine PluginExcpetion
     * ausgelöst.
     * <p>
     * Query Example:
     * <p>
     * <code>NOT $uniqueid:"9f72fa50-4845-41ea-b6b9-ccd518c353be" AND 
             txtcooperatespace:"9dba107e-f8ef-4150-a832-040d75a6eda7" AND invoice.number:"45"</code>
     * 
     * <p>
     * in case a duplicate invoice was detected the item invoice.number.duplicate is
     * filled. This item is used for a conditional event. The case is displayed as a
     * warning in the form.
     * 
     * @throws PluginException
     * @throws QueryException
     * 
     */
    private void validateInvoiceNumber(ItemCollection workitem) throws PluginException {

        String invoiceNumber = workitem.getItemValueString("invoice.number");
        String invoiceNumberDuplicate = workitem.getItemValueString("invoice.number.duplicate");

        // wenn keine Rechnungsnummer eingegeben wurde gehts weiter!
        if (invoiceNumber.isEmpty()) {
            return;
        }

        // rechnungseingang only workitems...
        String query = "(type:workitem OR type:workitemarchive) AND ($modelversion:rechnungseingang*) AND NOT ($uniqueid:\""
                + workitem.getUniqueID() + "\") AND (invoice.number:\"" + invoiceNumber + "\")";
        try {
            int result = this.getWorkflowService().getDocumentService().count(query, 1);
            if (result > 0) {
                // wenn _invoicenumber_duplicate bereits gesetzt ist - dann geht es ohne prüfung
                // weiter
                if (!invoiceNumberDuplicate.isEmpty()) {
                    logger.warning("...validateion skipped by user with duplicate invoice number: " + invoiceNumber);
                } else {

                    // set _invoicenumber_duplicate - dadurch wird die warnmeldung ausgegeben und
                    // der Vorgang nicht weitergeleitet
                    workitem.replaceItemValue("invoice.number.duplicate", invoiceNumber);
                    throw new PluginException(InvoicePlugin.class.getName(), ERROR_DUPPLICATE_INVOICE_NUMBER,
                            "ACHTUNG: Die Rechnungsnummer wurde bereits gebucht. Bitte prüfen Sie den Beleg. Sollte die Belegnummer korrekt sein, wiederholen Sie die Aktion.");
                }
            } else {
                // clear !
                workitem.appendItemValue("invoice.number.duplicate.history",
                        workitem.getItemValueString("invoice.number.duplicate"));
                workitem.replaceItemValue("invoice.number.duplicate", "");
            }
        } catch (QueryException e) {
            throw new PluginException(PluginException.class.getName(), "QUERY ERROR", e.getMessage(), e);
        }
    }

    /**
     * Hilfsmethod prüft ob die cdtr.nummer mit einer nummer aus der Cargosoft
     * Import Datei übereinstimmt.
     * 
     * @param workitem
     * @throws PluginException
     */
    private void validateCargosoftCdtrNumber(ItemCollection workitem) throws PluginException {
        String crdtrNumber = workitem.getItemValueString("cdtr.number");

        // wenn keine Nummer eingegeben wurde gehts weiter!
        if (crdtrNumber.isEmpty()) {
            return;
        }

        // search creditor number in cargosoft...
        // die cargosoft Kreditornummer beginnt seltsamerweise mit einem K.....
        try {
            String query = "(type:cargosoftkreditor) AND (name:K" + crdtrNumber + " OR name:" + crdtrNumber +")";
            List<ItemCollection> result = this.getWorkflowService().getDocumentService().find(query, 1, 0);
            if (result == null || result.size() == 0) {
                throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA,
                        "Die Kreditorennummer ist nicht gültig.");

            } else {
                // update cargosoft crediotr name
                ItemCollection cargo = result.get(0);
                workitem.setItemValue("cdtr.name.cargosoft", cargo.getItemValueString("_VENDOR_Name"));
            }
        } catch (QueryException e) {
            e.printStackTrace();
        }

    }

    /**
     * converts the Map List of a workitem into a List of ItemCollectons
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected static List<ItemCollection> explodeChildList(ItemCollection workitem) {
        // convert current list of childItems into ItemCollection elements
        ArrayList<ItemCollection> childItems = new ArrayList<ItemCollection>();

        List<Object> mapOrderItems = workitem.getItemValue(CHILD_ITEM_PROPERTY);
        for (Object mapOderItem : mapOrderItems) {
            if (mapOderItem instanceof Map) {
                ItemCollection itemCol = new ItemCollection((Map) mapOderItem);
                childItems.add(itemCol);
            }
        }
        return childItems;
    }
}
