package cms.si.payment;

import cms.AmountMoreThanBalanceException;

import cms.db.*;

import cms.db.payments.px.PayConRequest;

import cms.db.payments.px.PayConResponse;

import cms.db.payments.px.PayConStatus;

import cms.hibernate.HibernateSessionFactory;

import cms.si.payment.px.PayConPaymentSystem;

import cms.si.payment.px.PaymentException;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.hibernate.Query;

import org.hibernate.Session;

import javax.servlet.http.HttpServletRequest;

import java.util.*;

/**
 * @author alf
 * @author Alexey Malkovskiy
 */
public class PaymentManager {

    private static final Log log = LogFactory.getLog(PaymentManager.class);

    public static final int TRANSACTION_PENDING = 1;

    public static final int TRANSACTION_PROCESSED = 2;

    public static final int TRANSACTION_DECLINED = 3;


    public BillingInfo billCreditCard(Player player, Long transactionID, CreditCard card,

                                      double amount, String ipAddress, String customerRef) throws WrongAmountException, PaymentException {

        player.setUpdateDate(new Date());

        PayConPaymentSystem px = PayConPaymentSystem.getInstance();

        String strTransactionID = px.getTrackID(transactionID);

        // here comes authorization code
        PayConRequest request = px.createAuthRequest(player.getCurrency().getId(),

                strTransactionID, card, amount, ipAddress, player.getUsername());

        PayConResponse response = px.sendRequest(request);

        if (response.getReturnCode() != ReturnCode.RETURN_CODE_SUCCESS ||

                response.getStatus().getAuthCode() != AuthorizationFailureObject.SUCCESS)

            throw new PaymentException();

        double paid = response.getStatus().getAmountPaid();

        if (amount != paid)

            throw new WrongAmountException(amount, paid);

        player.incrementBalance(paid);

        log.info("billCreditCard player " + player.getId() + " TrackID " + strTransactionID

                + " added " + amount

                + " to balance because the credit card was charged for this amount");

        return new BillingInfo(response.getStatus().getAmountPaid(),

                response.getStatus().getAmountReq(),

                response.getStatus().getTransactionId());

    }


    public void performDeposit(double amount, Player player, CreditCardInfo info,

                               HttpServletRequest request, String remoteHost) throws PaymentException, WrongAmountException {

        org.hibernate.Transaction tx = HibernateSessionFactory.currentSession().beginTransaction();

        Transaction t = new Transaction();

        t.setAmount(amount);

        t.setType(TransactionType.DEPOSIT);

        t.setStatus(TransactionStatus.PENDING);

        t.setCreateDate(new Date());

        t.setPlayer(player);

        saveNow(t);

        try {
            BillingInfo billingInfo = billCreditCard(player, t.getId(), info, amount, remoteHost,

                    player.getUsername());

            t.setStatus(TransactionStatus.COMPLETED);

            t.setExternalId(billingInfo.getTransactionId());

            t.setProcessDate(new Date());

        } catch (PaymentException e) {
            log.error("Payment problem", e);

            // TODO
        } catch (WrongAmountException e) {
            log.error("Wrong amount", e);
            // TODO
        }

        tx.commit

                ();
    }


    private void saveNow(Transaction t) {
        Session session = HibernateSessionFactory.currentSession();

        session.save(t);
        session.flush();
    }

    @SuppressWarnings("unchecked")
    public UserCashOut performCashOut(double amount, Player player) throws AmountMoreThanBalanceException {
        Session ses = HibernateSessionFactory.currentSession();

        if (player.getBalanceAmt() < amount) {
            throw new AmountMoreThanBalanceException();
        }

        org.hibernate.Transaction tx = ses.beginTransaction();

        UserCashOut userCashOut = new UserCashOut(

        );
        userCashOut.setAmount(amount);
        userCashOut.setCreateDate(new Date());
        userCashOut.setPlayer(player);
        player.getCashOuts().add(userCashOut);
        ses.save(userCashOut);
        double checkAmount = 0;
        double amountForPay = amount;
        boolean freeDebitsExist = true;
        //
        while (amountForPay > 0 && freeDebitsExist) {
            List<CashOutDebitInfo> freeDebits = findFreeDebits(player, ses);
            if (freeDebits.isEmpty()) {
                freeDebitsExist = false;
                checkAmount += amountForPay;
            } else {
                double oldAmount = amountForPay;
                amountForPay = createCredits(amountForPay, player, freeDebits, userCashOut, ses);
                amountForPay += payCredits(freeDebits, userCashOut, ses);
                if (amountForPay == oldAmount) {
                    //This is strange and hypothetical situation. It can mean
                    //that PaymentConnexions doesn't work properly. Pay by check.
                    freeDebitsExist = false;
                    checkAmount += amountForPay;
                }
            }
            ses.flush();
        }
        //
        if (checkAmount > 0) {
            payCheck(player, ses, userCashOut, checkAmount);
        }
        player.setBalanceAmt(player.getBalanceAmt() - amount);
        tx.commit();
        return userCashOut;
    }

    private void payCheck(Player player, Session ses, UserCashOut userCashOut, double checkAmount) {
        Check check = new Check();
        check.setAmount(checkAmount);
        check.setCreateDate(new Date());
        check.setPlayer(player);
        player.getChecks().add(check);
        check.setUserCashOut(userCashOut);
        userCashOut.setCheck(check);
        Transaction transaction = new Transaction();
        transaction.setAmount(-checkAmount);
        transaction.setCreateDate(new Date());
        transaction.setProcessDate(new Date());
        transaction.setType(TransactionType.CASHOUT);
        transaction.setStatus(TransactionStatus.PENDING);
        transaction.setPlayer(player);
        check.setTransaction(transaction);
        transaction.setCheck(check);
        ses.save(transaction);
        ses.save(check);
    }

    private double payCredits(List<CashOutDebitInfo> freeDebits, UserCashOut userCashOut, Session ses) {
        double unpaidAmount = 0;
        for (CashOutDebitInfo info : freeDebits) {
            try {
                String trackId = "Tr" + info.credit.getId();
                PayConRequest creditRequest = PayConPaymentSystem.getInstance().createReversalRequest(userCashOut.getPlayer().getCurrency().getId(), trackId, -info.credit.getAmount(), new Long(info.debit.getExternalId()));
                PayConResponse response = PayConPaymentSystem.getInstance().sendRequest(creditRequest);
                final PayConStatus status = response.getStatus();
                if (response.getReturnCode() == ReturnCode.RETURN_CODE_SUCCESS && status.getAuthCode() == AuthorizationFailureObject.SUCCESS) {
                    info.credit.setAmount(-status.getAmountPaid());
                    info.credit.setExternalId(status.getTransactionId());
                    info.credit.setStatus(TransactionStatus.COMPLETED);
                    info.credit.setProcessDate(new Date());
                } else {
                    switch (status.getAuthCode()) {
                        case TRANSACTION_PENDING:
                            info.credit.setStatus(TransactionStatus.PENDING);
                            break;
                        case TRANSACTION_PROCESSED:
                            //This is hypothetical situation. We have to delete this credit
                            //transaction. Original debit transaction still free for
                            //future credits.
                            ses.delete(info.credit);
                            info.credit = null;
                            break;
                        case TRANSACTION_DECLINED:
                        default:
                            info.debit.getCreditTransactions().remove(info.credit);
                            info.credit.setOriginalDebitTransaction(null);
                            info.debit.setCreditStatus("ERROR - " + status.getAuthCode());
                            userCashOut.getCredits().remove(info.credit);
                            info.credit.setUserCashOut(null);
                            unpaidAmount += -info.credit.getAmount();
                            ses.delete(info.credit);
                            break;
                    }
                }
            } catch (PaymentException e) {
                // ignore
                //log.error(e.getMessage(), e);
            }
        }
        return unpaidAmount;
    }

    private double createCredits(double amount, Player player, List<CashOutDebitInfo> freeDebits, UserCashOut userCashOut, Session ses) {
        double checkAmount = amount;
        int i = 0;
        for (CashOutDebitInfo debitInfo : freeDebits) {
            Transaction credit = new Transaction();
            if (checkAmount > debitInfo.remainder) {
                credit.setAmount(-debitInfo.remainder);
                checkAmount -= debitInfo.remainder;
            } else {
                credit.setAmount(-checkAmount);
                checkAmount = 0;
            }
            credit.setCreateDate(new Date());
            credit.setOriginalDebitTransaction(debitInfo.debit);
            debitInfo.debit.getCreditTransactions().add(credit);
            credit.setPlayer(player);
            credit.setType(TransactionType.CASHOUT);
            credit.setStatus(TransactionStatus.PENDING);
            credit.setUserCashOut(userCashOut);
            userCashOut.getCredits().add(credit);
            debitInfo.credit = credit;
            ses.save(credit);
            i++;
            if (checkAmount <= 0) {
                break;
            }
        }
        int size = freeDebits.size();
        for (int j = i; j < size; j++) {
            freeDebits.remove(i);
        }
        return checkAmount;
    }

    @SuppressWarnings("unchecked")
    private List<CashOutDebitInfo> findFreeDebits(Player player, Session ses) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MONTH, -6);
        Query query = ses.createQuery("from Transaction as t where " + "t.player = :player and t.createDate > :date " + "and t.creditStatus is null and t.type = :type and " + "(t.amount > (select sum(-t2.amount) from t.creditTransactions as t2) " + "or t.creditTransactions is empty) "
                //+ "where t2.status = :status) or t.creditTransactions is empty) "
                + "and t.status = :status " + "and t.externalId is not null " + "order by t.createDate");
        query.setEntity("player", player);
        query.setDate("date", calendar.getTime());
        query.setInteger("type", TransactionType.DEPOSIT.getId());
        query.setInteger("status", TransactionStatus.COMPLETED.getId());
        //query.setInteger("status", TransactionStatus.COMPLETED.getId());
        List<Transaction> transactions = query.list();
        List<CashOutDebitInfo> freeDebits = new ArrayList<CashOutDebitInfo>();
        for (Transaction transaction : transactions) {
            Set<Transaction> credits = transaction.getCreditTransactions();
            double sum = 0;
            for (Transaction credit : credits) {
                sum += credit.getAmount();
            }
            CashOutDebitInfo info = new CashOutDebitInfo();
            info.debit = transaction;
            info.remainder = transaction.getAmount() - sum;
            freeDebits.add(info);
        }
        return freeDebits;
    }
}