/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *******************************************************************************/
package org.apache.ofbiz.webpos.transaction;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.GeneralException;
import org.apache.ofbiz.base.util.UtilDateTime;
import org.apache.ofbiz.base.util.UtilGenerics;
import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.util.EntityUtil;
import org.apache.ofbiz.order.shoppingcart.CheckOutHelper;
import org.apache.ofbiz.order.shoppingcart.ShoppingCart;
import org.apache.ofbiz.order.shoppingcart.ShoppingCartItem;
import org.apache.ofbiz.order.shoppingcart.ShoppingCart.CartPaymentInfo;
import org.apache.ofbiz.product.store.ProductStoreWorker;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.service.ServiceUtil;
import org.apache.ofbiz.webpos.session.WebPosSession;

public class WebPosTransaction {

    private static final String RESOURCE = "WebPosUiLabels";
    private static final String MODULE = WebPosTransaction.class.getName();
    public static final int NO_PAYMENT = 0;
    public static final int INTERNAL_PAYMENT = 1;
    public static final int EXTERNAL_PAYMENT = 2;

    private CheckOutHelper ch = null;
    private GenericValue txLog = null;
    private String transactionId = null;
    private String orderId = null;
    private String partyId = null;
    private boolean isOpen = false;
    private int drawerIdx = 0;
    private GenericValue shipAddress = null;
    private WebPosSession webPosSession = null;

    public WebPosTransaction(WebPosSession session) {
        this.webPosSession = session;
        this.partyId = "_NA_";
        Delegator delegator = session.getDelegator();
        ShoppingCart cart = session.getCart();
        this.ch = new CheckOutHelper(session.getDispatcher(), delegator, cart);
        cart.setChannelType("POS_SALES_CHANNEL");
        cart.setFacilityId(session.getFacilityId());
        cart.setTerminalId(session.getId());

        if (session.getUserLogin() != null) {
            cart.addAdditionalPartyRole(session.getUserLogin().getString("partyId"), "SALES_REP");
        }

        // setup the TX log
        this.transactionId = delegator.getNextSeqId("PosTerminalLog");
        txLog = delegator.makeValue("PosTerminalLog");
        txLog.set("posTerminalLogId", this.transactionId);
        txLog.set("posTerminalId", session.getId());
        txLog.set("transactionId", transactionId);
        txLog.set("userLoginId", session.getUserLoginId());
        txLog.set("statusId", "POSTX_ACTIVE");
        txLog.set("logStartDateTime", UtilDateTime.nowTimestamp());
        try {
            txLog.create();
            cart.setTransactionId(transactionId);
        } catch (GenericEntityException e) {
            Debug.logError(e, "Unable to create TX log - not fatal", MODULE);
        }

        Debug.logInfo("Created WebPosTransaction [" + this.transactionId + "]", MODULE);
    }
    /** get user login id */
    public String getUserLoginId() {
        return webPosSession.getUserLoginId();
    }
    /** get drawer number */
    public int getDrawerNumber() {
        return drawerIdx + 1;
    }
    /** get transaction id */
    public String getTransactionId() {
        return this.transactionId;
    }
    /** get terminal id */
    public String getTerminalId() {
        return webPosSession.getId();
    }
    /** get facilityId */
    public String getFacilityId() {
        return webPosSession.getFacilityId();
    }
    /** get terminal log id */
    public String getTerminalLogId() {
        return txLog.getString("posTerminalLogId");
    }
    /** is open */
    public boolean isOpen() {
        this.isOpen = false;
        GenericValue terminalState = this.getTerminalState();
        if (terminalState != null) {
            if ((terminalState.getDate("closedDate")) == null) {
                this.isOpen = true;
            }
        }
        return this.isOpen;
    }
    /** get terminal state */
    public GenericValue getTerminalState() {
        Delegator delegator = webPosSession.getDelegator();
        List<GenericValue> states = null;
        try {
            states = delegator.findList("PosTerminalState", EntityCondition.makeCondition(UtilMisc.toMap("posTerminalId", webPosSession.getId(),
                    "startingTxId", getTransactionId())), null, null, null, false);
        } catch (GenericEntityException e) {
            Debug.logError(e, MODULE);
        }
        states = EntityUtil.filterByDate(states, UtilDateTime.nowTimestamp(), "openedDate", "closedDate", true);
        return EntityUtil.getFirst(states);
    }
    /** close tax */
    public void closeTx() {
        if (UtilValidate.isNotEmpty(txLog)) {
            txLog.set("statusId", "POSTX_CLOSED");
            txLog.set("itemCount", (long) getCart().size());
            txLog.set("logEndDateTime", UtilDateTime.nowTimestamp());
            try {
                txLog.store();
            } catch (GenericEntityException e) {
                Debug.logError(e, "Unable to store TX log - not fatal", MODULE);
            }
            getCart().clear();
            Debug.logInfo("Transaction closed", MODULE);
        }
    }
    /** paid in out */
    public void paidInOut(String type) {
        if (UtilValidate.isNotEmpty(txLog)) {
            txLog.set("statusId", "POSTX_PAID_" + type);
            txLog.set("logEndDateTime", UtilDateTime.nowTimestamp());
            try {
                txLog.store();
            } catch (GenericEntityException e) {
                Debug.logError(e, "Unable to store TX log - not fatal", MODULE);
            }
            webPosSession.setCurrentTransaction(null);
            Debug.logInfo("Paid " + type, MODULE);
        }
    }
    /** modify price */
    public void modifyPrice(int cartLineIdx, BigDecimal price) {
        ShoppingCartItem item = getCart().findCartItem(cartLineIdx);
        if (UtilValidate.isNotEmpty(item)) {
            Debug.logInfo("Modify item price " + item.getProductId() + "/" + price, MODULE);
            item.setBasePrice(price);
        } else {
            Debug.logInfo("Item " + cartLineIdx + " not found", MODULE);
        }
    }
    /** calc tax */
    public void calcTax() {
        try {
            ch.calcAndAddTax(this.getStoreOrgAddress());
        } catch (GeneralException e) {
            Debug.logError(e, MODULE);
        }
    }
    /** process sale */
    public BigDecimal processSale() throws GeneralException {
        Debug.logInfo("Process sale", MODULE);
        BigDecimal grandTotal = this.getGrandTotal();
        BigDecimal paymentAmt = this.getPaymentTotal();
        if (grandTotal.compareTo(paymentAmt) > 0) {
            throw new GeneralException(UtilProperties.getMessage(RESOURCE, "WebPosNotEnoughFunds", webPosSession.getLocale()));
        }

        // attach the party ID to the cart
        getCart().setOrderPartyId(partyId);

        // validate payment methods
        Debug.logInfo("Validating payment methods", MODULE);
        Map<String, ? extends Object> valRes = UtilGenerics.cast(ch.validatePaymentMethods());
        if (valRes != null && ServiceUtil.isError(valRes)) {
            throw new GeneralException(ServiceUtil.getErrorMessage(valRes));
        }

        // store the order
        Debug.logInfo("Store order", MODULE);
        Map<String, ? extends Object> orderRes = UtilGenerics.cast(ch.createOrder(webPosSession.getUserLogin()));

        if (orderRes != null && ServiceUtil.isError(orderRes)) {
            throw new GeneralException(ServiceUtil.getErrorMessage(orderRes));
        } else if (orderRes != null) {
            this.orderId = (String) orderRes.get("orderId");
        }

        // process the payment(s)
        Debug.logInfo("Processing the payment(s)", MODULE);
        Map<String, ? extends Object> payRes = null;
        try {
            payRes = UtilGenerics.cast(ch.processPayment(ProductStoreWorker.getProductStore(webPosSession.getProductStoreId(),
                    webPosSession.getDelegator()), webPosSession.getUserLogin(), true));
        } catch (GeneralException e) {
            Debug.logError(e, MODULE);
            throw e;
        }

        if (payRes != null && ServiceUtil.isError(payRes)) {
            throw new GeneralException(ServiceUtil.getErrorMessage(payRes));
        }

        // get the change due
        BigDecimal change = grandTotal.subtract(paymentAmt);

        // TODO notify the change due

        // threaded drawer/receipt printing
        // TODO open the drawer
        // TODO print the receipt

        // save the TX Log
        txLog.set("statusId", "POSTX_SOLD");
        txLog.set("orderId", orderId);
        txLog.set("itemCount", (long) getCart().size());
        txLog.set("logEndDateTime", UtilDateTime.nowTimestamp());
        try {
            txLog.store();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Unable to store TX log - not fatal", MODULE);
        }

        return change;
    }

    private synchronized GenericValue getStoreOrgAddress() {
        if (UtilValidate.isEmpty(this.shipAddress)) {
            // locate the store's physical address - use this for tax
            GenericValue facility = null;
            try {
                facility = webPosSession.getDelegator().findOne("Facility", UtilMisc.toMap("facilityId", webPosSession.getFacilityId()), false);
            } catch (GenericEntityException e) {
                Debug.logError(e, MODULE);
            }
            if (UtilValidate.isEmpty(facility)) {
                return null;
            }

            List<GenericValue> fcp = null;
            try {
                fcp = facility.getRelated("FacilityContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", "SHIP_ORIG_LOCATION"),
                        null, false);
            } catch (GenericEntityException e) {
                Debug.logError(e, MODULE);
            }
            fcp = EntityUtil.filterByDate(fcp);
            GenericValue purp = EntityUtil.getFirst(fcp);
            if (UtilValidate.isNotEmpty(purp)) {
                try {
                    this.shipAddress = webPosSession.getDelegator().findOne("PostalAddress",
                            UtilMisc.toMap("contactMechId", purp.getString("contactMechId")), false);
                } catch (GenericEntityException e) {
                    Debug.logError(e, MODULE);
                }
            }
        }
        return this.shipAddress;
    }
    /** clear payments */
    public void clearPayments() {
        Debug.logInfo("all payments cleared from sale", MODULE);
        getCart().clearPayments();
    }
    /** clear payment */
    public void clearPayment(int index) {
        Debug.logInfo("removing payment " + index, MODULE);
        getCart().clearPayment(index);
    }
    /** clear payment */
    public void clearPayment(String id) {
        Debug.logInfo("removing payment " + id, MODULE);
        getCart().clearPayment(id);
    }
    /** get payment info */
    public CartPaymentInfo getPaymentInfo(int index) {
        return getCart().getPaymentInfo(index);
    }
    /** get payment method type id */
    public String getPaymentMethodTypeId(int index) {
        return getCart().getPaymentInfo(index).getPaymentMethodTypeId();
    }
    /** check payment method type */
    public int checkPaymentMethodType(String paymentMethodTypeId) {
        Map<String, ? extends Object> fields = UtilMisc.toMap("paymentMethodTypeId", paymentMethodTypeId, "productStoreId",
                webPosSession.getProductStoreId());
        List<GenericValue> values = null;
        try {
            values = webPosSession.getDelegator().findList("ProductStorePaymentSetting", EntityCondition.makeCondition(fields), null, null, null,
                    true);
        } catch (GenericEntityException e) {
            Debug.logError(e, MODULE);
        }

        final String externalCode = "PRDS_PAY_EXTERNAL";
        if (UtilValidate.isEmpty(values)) {
            return NO_PAYMENT;
        } else {
            boolean isExternal = true;
            Iterator<GenericValue> i = values.iterator();
            while (i.hasNext() && isExternal) {
                GenericValue v = i.next();
                //Debug.logInfo("Testing [" + paymentMethodTypeId + "] - " + v, MODULE);
                if (!externalCode.equals(v.getString("paymentServiceTypeEnumId"))) {
                    isExternal = false;
                }
            }

            if (isExternal) {
                return EXTERNAL_PAYMENT;
            } else {
                return INTERNAL_PAYMENT;
            }
        }
    }

    public static int getNoPaymentCode() {
        return NO_PAYMENT;
    }

    public static int getExternalPaymentCode() {
        return EXTERNAL_PAYMENT;
    }

    public static int getInternalPaymentCode() {
        return INTERNAL_PAYMENT;
    }
    /** add payment */
    public BigDecimal addPayment(String id, BigDecimal amount) {
        return this.addPayment(id, amount, null, null);
    }
    /** add payment */
    public BigDecimal addPayment(String id, BigDecimal amount, String refNum, String authCode) {
        Debug.logInfo("Added payment " + id + "/" + amount, MODULE);
        if ("CASH".equals(id)) {
            // clear cash payments first; so there is only one
            getCart().clearPayment(id);
        }
        getCart().addPaymentAmount(id, amount, refNum, authCode, true, true, false);
        return this.getTotalDue();
    }
    /** process amount */
    public BigDecimal processAmount(BigDecimal amount) throws GeneralException {
        if (UtilValidate.isEmpty(amount)) {
            Debug.logInfo("Amount is empty; assumption is full amount : " + this.getTotalDue(), MODULE);
            amount = this.getTotalDue();
            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
                throw new GeneralException();
            }
        }
        return amount;
    }
    /** process no payment */
    public synchronized void processNoPayment(String paymentMethodTypeId) {
        try {
            BigDecimal amount = processAmount(null);
            Debug.logInfo("Processing [" + paymentMethodTypeId + "] Amount : " + amount, MODULE);

            // add the payment
            addPayment(paymentMethodTypeId, amount, null, null);
        } catch (GeneralException e) {
            // errors handled
        }
    }
    /** process external payment */
    public synchronized void processExternalPayment(String paymentMethodTypeId, BigDecimal amount, String refNum) {
        if (refNum == null) {
            //TODO handle error message
            return;
        }

        try {
            amount = processAmount(amount);
            Debug.logInfo("Processing [" + paymentMethodTypeId + "] Amount : " + amount, MODULE);

            // add the payment
            addPayment(paymentMethodTypeId, amount, refNum, null);
        } catch (GeneralException e) {
            // errors handled
        }
    }
    /** make credit card vo */
    public String makeCreditCardVo(String cardNumber, String expDate, String firstName, String lastName) {
        LocalDispatcher dispatcher = webPosSession.getDispatcher();
        String expMonth = expDate.substring(0, 2);
        String expYear = expDate.substring(2);
        // two digit year check -- may want to re-think this
        if (expYear.length() == 2) {
            expYear = "20" + expYear;
        }

        Map<String, Object> svcCtx = new HashMap<>();
        svcCtx.put("userLogin", webPosSession.getUserLogin());
        svcCtx.put("partyId", partyId);
        svcCtx.put("cardNumber", cardNumber);
        svcCtx.put("firstNameOnCard", firstName == null ? "" : firstName);
        svcCtx.put("lastNameOnCard", lastName == null ? "" : lastName);
        svcCtx.put("expMonth", expMonth);
        svcCtx.put("expYear", expYear);
        svcCtx.put("cardType", UtilValidate.getCardType(cardNumber));

        Map<String, Object> svcRes = null;
        try {
            svcRes = dispatcher.runSync("createCreditCard", svcCtx);
        } catch (GenericServiceException e) {
            Debug.logError(e, MODULE);
            return null;
        }
        if (ServiceUtil.isError(svcRes)) {
            Debug.logError(ServiceUtil.getErrorMessage(svcRes) + " - " + svcRes, MODULE);
            return null;
        } else {
            return (String) svcRes.get("paymentMethodId");
        }
    }
    /** set payment ref num */
    public void setPaymentRefNum(int paymentIndex, String refNum, String authCode) {
        Debug.logInfo("setting payment index reference number " + paymentIndex + " / " + refNum + " / " + authCode, MODULE);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.setRefNum(refNum, authCode);
    }

    /** CVV2 code should be entered when a card can't be swiped */
    public void setPaymentSecurityCode(String paymentId, String refNum, String securityCode) {
        Debug.logInfo("setting payment security code " + paymentId, MODULE);
        int paymentIndex = getCart().getPaymentInfoIndex(paymentId, refNum);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.setSecurityCode(securityCode);
        inf.setIsSwiped(false);
    }

    /** Track2 data should be sent to processor when a card is swiped. */
    public void setPaymentTrack2(String paymentId, String refNum, String securityCode) {
        Debug.logInfo("setting payment security code " + paymentId, MODULE);
        int paymentIndex = getCart().getPaymentInfoIndex(paymentId, refNum);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.setSecurityCode(securityCode);
        inf.setIsSwiped(true);
    }

    /** Postal code should be entered when a card can't be swiped */
    public void setPaymentPostalCode(String paymentId, String refNum, String postalCode) {
        Debug.logInfo("setting payment security code " + paymentId, MODULE);
        int paymentIndex = getCart().getPaymentInfoIndex(paymentId, refNum);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.setPostalCode(postalCode);
    }
    /** get tax total */
    public BigDecimal getTaxTotal() {
        return getCart().getTotalSalesTax();
    }
    /** get grand total */
    public BigDecimal getGrandTotal() {
        return getCart().getGrandTotal();
    }
    /** get number of payments */
    public int getNumberOfPayments() {
        return getCart().selectedPayments();
    }
    /** get payment total */
    public BigDecimal getPaymentTotal() {
        return getCart().getPaymentTotal();
    }
    /** get total quantity */
    public BigDecimal getTotalQuantity() {
        return getCart().getTotalQuantity();
    }
    /** get total due */
    public BigDecimal getTotalDue() {
        BigDecimal grandTotal = this.getGrandTotal();
        BigDecimal paymentAmt = this.getPaymentTotal();
        return grandTotal.subtract(paymentAmt);
    }
    /** add product promo code */
    public String addProductPromoCode(String code) {
        String result = getCart().addProductPromoCode(code, webPosSession.getDispatcher());
        calcTax();
        return result;
    }

    /** get cart */
    public ShoppingCart getCart() {
        return webPosSession.getCart();
    }
}
