package performa.utils;

import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.Card;
import com.stripe.model.Charge;
import com.stripe.model.Coupon;
import com.stripe.model.Customer;
import com.stripe.model.Event;
import com.stripe.model.Invoice;
import com.stripe.model.Plan;
import com.stripe.model.Subscription;
import com.stripe.model.UsageRecord;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import oneit.appservices.config.ConfigMgr;
import oneit.logging.LogLevel;
import oneit.logging.LogMgr;
import oneit.logging.LoggingArea;
import oneit.objstore.ObjectTransaction;
import oneit.objstore.rdbms.filters.EqualsFilter;
import oneit.security.SecUser;
import oneit.utils.BusinessException;
import oneit.utils.DateDiff;
import oneit.utils.math.NullArith;
import oneit.utils.parsers.FieldException;
import performa.orm.Company;
import performa.orm.CurrencyPlanNPO;
import performa.orm.HiringTeam;
import performa.orm.Job;
import performa.orm.PaymentPlan;
import performa.orm.StripePlanNPO;
import performa.orm.types.CurrencyType;
import spark.utils.IOUtils;


public class StripeUtils 
{
    public static final String  STRIPE_KEY          =   ConfigMgr.getKeyfileString("stripe.key","");
    public static final String  STRIPE_PUB_KEY      =   ConfigMgr.getKeyfileString("stripe.pubkey","");
    public static final String  STRIPE_PLAN_ID      =   ConfigMgr.getKeyfileString("stripe.plan.id","0001");
    public static final String  STRIPE_COUPON_ID    =   ConfigMgr.getKeyfileString("stripe.coupon.id","EAP");

    static
    {
        Stripe.apiKey   =   STRIPE_KEY;
    }
    
    public static void createPlan(StripePlanNPO stripePlan, CurrencyPlanNPO currencyPlan) throws FieldException, BusinessException
    {
        try 
        {
            Map<String, Object> overagePlanParams = new HashMap<>();
            
            overagePlanParams.put("amount", NullArith.intVal(NullArith.multiply(currencyPlan.getPerJobAmount(), 100, 0d)));
            overagePlanParams.put("interval", "month");
            overagePlanParams.put("usage_type", "metered");
            overagePlanParams.put("product", stripePlan.getProductID());
            overagePlanParams.put("currency", currencyPlan.getCurrencyType().getName());
            overagePlanParams.put("nickname", stripePlan.getPlanName() + " Overage");
            
            Plan    overagePlan =   Plan.create(overagePlanParams);
            
            Map<String, Object> planParams = new HashMap<>();
            
            planParams.put("amount", NullArith.intVal(NullArith.multiply(currencyPlan.getAmount(), 100, 0d)));
            planParams.put("interval", "month");
            planParams.put("product", stripePlan.getProductID());
            planParams.put("currency", currencyPlan.getCurrencyType().getName());
            planParams.put("nickname", stripePlan.getPlanName());
            
            Map<String, Object> planMetadata = new HashMap<>();
            
            planMetadata.put("ActiveJobs", stripePlan.getActiveJobs());
            planMetadata.put("LinkedPlan", overagePlan.getId());

            planParams.put("metadata", planMetadata);
            
            Plan    plan =   Plan.create(planParams);
            
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Create plan in stripe : ", plan + " overage plan: " + overagePlan);
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while creating a plan in stripe");
                        
            throw new BusinessException("Problem with creating plan. " + ex.getMessage());
        }
    }
    
    public static void createCustomer(HiringTeam hiringTeam) throws FieldException
    {
        try 
        {
            SecUser             secUser         =   hiringTeam.getAddedByUser().getUser();
            Map<String, Object> customerParams  =   new HashMap<>();

            customerParams.put("description", hiringTeam);
            customerParams.put("email", secUser.getEmail());
            
            Customer    customer    =   Customer.create(customerParams);
            
            hiringTeam.setStripeReference(customer.getId());
            
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Create customer in stripe : ", customer);
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while creating a customer in stripe");
        }
    }
    
    
    public static Card updateCardDetails(HiringTeam hiringTeam, String token) throws FieldException, BusinessException
    {
        try 
        {
            if(hiringTeam.getStripeReference() == null)
            {
                createCustomer(hiringTeam);
            }
            
            Customer customer = Customer.retrieve(hiringTeam.getStripeReference());
            
            Map<String, Object> updateParams = new HashMap<>();
            
            updateParams.put("source", token);

            customer    =   customer.update(updateParams);
            
            Card    card    =   (Card) customer.getSources().retrieve(customer.getDefaultSource());
            
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Update card details in stripe, customer  : ", customer, " card : ", card);

            return card;
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while updating card details in stripe");
            
            throw new BusinessException("Problem with updating card details. " + ex.getMessage());
        }
    }
    
    
    public static Card retrieveCard(HiringTeam hiringTeam) throws FieldException
    {
        try 
        {
            Customer customer = Customer.retrieve(hiringTeam.getStripeReference());
            
            return (Card) customer.getSources().retrieve(customer.getDefaultSource());
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while retrieving card in stripe");
        } 
        
        return null;
    }
    
    public static Coupon retrieveCoupon(String couponCode) throws FieldException
    {
        try 
        {
            return Coupon.retrieve(couponCode);
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while retrieving coupon in stripe coupon code: " + couponCode);
        } 
        
        return null;
    }
    
    public static Subscription retrieveSubscription(String subscriptionRef) throws FieldException
    {
        try 
        {
            return Subscription.retrieve(subscriptionRef);
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while retrieving a subscription in stripe");
        } 
        
        return null;
    }
    
    
    public static List<Invoice> retrieveInvoices(HiringTeam hiringTeam) throws FieldException
    {
        if(hiringTeam.getStripeSubscription() != null)
        {
            try 
            {
                Map<String, Object> invoiceParams = new HashMap<>();

                invoiceParams.put("customer", hiringTeam.getStripeReference());

                return Invoice.list(invoiceParams).getData();
            } 
            catch (StripeException ex) 
            {
                LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while retriving invoices in stripe for subscription: " + hiringTeam.getStripeSubscription());
            } 
        }
        
        return new ArrayList<>();
    }
    
    
    public static void subscribeCustomer(Company company) throws FieldException
    {
        try 
        {
            Plan    plan        =   Plan.retrieve(STRIPE_PLAN_ID);
            Date    today       =   new Date(); 
            Date    trialExpiry =   DateDiff.add(today, Calendar.DATE, plan.getTrialPeriodDays().intValue());
            
            Map<String, Object> item    =   new HashMap<>();
            item.put("plan", STRIPE_PLAN_ID);

            Map<String, Object> items   =   new HashMap<>();
            items.put("0", item);

            Map<String, Object> params  =   new HashMap<>();
            params.put("items", items);
            params.put("coupon", STRIPE_COUPON_ID);
            params.put("customer", company.getStripeReference());
            params.put("trial_end", trialExpiry.getTime() / 1000L);

            Subscription    subscription    =   Subscription.create(params);
            
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Subscribing customer in stripe  : ", subscription);

            company.setStripeSubscription(subscription.getId());
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while creating subscrition in stripe");
        } 
    }
    
    
    public static Subscription updatePlan(HiringTeam hiringTeam, Subscription subscription, PaymentPlan paymentPlan) throws FieldException
    {
        try 
        {
            Map<String, Object> itemA   =   new HashMap<>();
            Map<String, Object> itemB   =   new HashMap<>();
            
            if(subscription != null)
            {
                itemA.put("id", subscription.getSubscriptionItems().getData().get(0).getId());
                itemB.put("id", subscription.getSubscriptionItems().getData().get(1).getId());
            }
            
            itemA.put("plan", paymentPlan.getStripeReference());
            itemB.put("plan", paymentPlan.getLinkedPlanReference());

            Map<String, Object> items   =   new HashMap<>();
            items.put("0", itemA);
            items.put("1", itemB);

            Map<String, Object> params  =   new HashMap<>();
            params.put("items", items);
            params.put("prorate", false);

            if(hiringTeam.getCoupon() != null)
            {
                params.put("coupon", hiringTeam.getCoupon().getCouponCode());
            }
            
            if(subscription != null)
            {
                subscription.update(params);
            }
            else 
            {
                params.put("customer", hiringTeam.getStripeReference());
                
                subscription    =   Subscription.create(params);
            }
            
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Subscribing customer in stripe  : ", subscription);

            hiringTeam.setStripeSubscription(subscription.getId());
            hiringTeam.setStripeFixedSubItem(subscription.getSubscriptionItems().getData().get(0).getId());
            hiringTeam.setStripeMeteredSubItem(subscription.getSubscriptionItems().getData().get(1).getId());
            
            return subscription;
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while creating subscrition in stripe");
        } 
        
        return null;
    }
    
    
    public static void applyCoupon(HiringTeam hiringTeam) throws BusinessException
    {
        try 
        {
            Subscription subscription = retrieveSubscription(hiringTeam.getStripeSubscription());
            
            if(subscription != null)
            {
                Map<String, Object> params  =   new HashMap<>();

                params.put("coupon", hiringTeam.getCouponCode());

                subscription.update(params);
                
                LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Updating subscription : ", subscription, " with coupon : ", hiringTeam.getCouponCode());
            }
        } 
        catch (FieldException | StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex,  "Error while applying coupon in stripe  for hiring team: "+ hiringTeam);
            
            throw new BusinessException("Problem with applying coupon. Please contact adminstrator for more info.");
        }
    }
    
    
    public static void chargeUpgradePlanDifference(HiringTeam hiringTeam, double costDifference) throws BusinessException
    {
        try 
        {
            Map<String, Object> chargeParams = new HashMap<>();

            chargeParams.put("amount", NullArith.intVal(NullArith.multiply(costDifference, 100, 0d)));
            chargeParams.put("currency", hiringTeam.getCountry().getCurrency().getDescription());
            chargeParams.put("description", "Charges of upgrading plan");
            chargeParams.put("customer", hiringTeam.getStripeReference());

            Charge.create(chargeParams);
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while charging plan upgrade difference in stripe");
            
            throw new BusinessException("Problem with charging for your plan upgrade. Please contact adminstrator for more info.");
        } 
    }
    
    
    public static void recordUsage(HiringTeam hiringTeam) throws BusinessException
    {
        try 
        {
            Map<String, Object> params  =   new HashMap<>();
            
            Date        now         =   new Date();
            HiringTeam  billingTeam =   hiringTeam.getManageOwnBilling() ? hiringTeam: hiringTeam.getBilledByTeam();

            params.put("quantity", 1);
            params.put("timestamp", now.getTime() / 1000L);
            params.put("subscription_item", billingTeam.getStripeMeteredSubItem());
            params.put("action", "increment");

            UsageRecord.create(params, null);
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex,  "Error while recoding usage in stripe  for hiring team: "+ hiringTeam);
            
            throw new BusinessException("Problem with opening your job. Please contact adminstrator for more info.");
        }
    }
    
    
    public static void cancelSubscription(Subscription subscription, boolean cancelSubscription) throws BusinessException
    {
        try 
        {
            Map<String, Object> params  =   new HashMap<>();
        
            params.put("cancel_at_period_end", cancelSubscription);
            
            subscription.update(params);
        } 
        catch (StripeException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex,  "Error while updating subscription in stripe  : "+ subscription + " to cancel_at_period_end " + cancelSubscription);
        
            throw new BusinessException("Problem with cancelling your subscription. Please contact adminstrator for more info.");
        }
    }

    
    public static void makePayment(HiringTeam hiringTeam, Job job) throws BusinessException
    {
        try
        {
            CurrencyType    currency    =   hiringTeam.getCountry().getCurrency();
            int             amount      =   NullArith.intVal(NullArith.multiply(hiringTeam.getPPJAmount(job), 100, 0d));
            int             minAmount   =   currency == CurrencyType.GBP ? 300 : 500;
            
            // Stripe has a min amount for charge GBP 0.30 and other AUD, UAD, CAD, SGD, EUR 0.50
            if(amount >= minAmount)
            {
                Map<String, Object> chargeParams = new HashMap<>();

                chargeParams.put("amount", amount);
                chargeParams.put("currency", currency);
                chargeParams.put("description", "Charges of creating job");
                chargeParams.put("customer", hiringTeam.getStripeReference());

                Charge.create(chargeParams);
            }
        }
        catch (StripeException e)
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, e, "Error while making a payment of hiringTeam stripe " + hiringTeam.getStripeReference());

            throw new BusinessException("Stripe payment failed, Please contact adminstrator for more info.");
        }
    }
    
    
    public static void handleWebhook(HttpServletRequest request, ObjectTransaction objTran) throws FieldException 
    {
        try 
        {
            String rawJson = IOUtils.toString(request.getInputStream());
            
            Event event = Event.GSON.fromJson(rawJson, Event.class);
        
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Event received from stripe  : ", event);

            if (event != null && event.getType().equals("invoice.payment_succeeded")) 
            {
                Invoice invoice = (Invoice) event.getData().getObject();

                LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Payment Succeeded event received from stripe with invoice  : ", invoice);
                
                if(invoice.getBilling().equals("charge_automatically"))
                {
                    HiringTeam[]    hiringTeams =   HiringTeam.SearchByAll().andStripeReference(new EqualsFilter<>(invoice.getCustomer())).search(objTran);

                    if(hiringTeams != null && hiringTeams.length > 0)
                    {
                        HiringTeam  hiringTeam  =   hiringTeams[0];
                        Date        invoiceDate =   new Date(invoice.getDate() * 1000L);

                        if(hiringTeam.getPlanRenewedOn() == null || !DateDiff.startOfDay(invoiceDate).equals(DateDiff.startOfDay(hiringTeam.getPlanRenewedOn())))
                        {
                            if(hiringTeam.getPlanRenewedOn() != null)
                            {
                                hiringTeam.setUsedCredits(0);
                                hiringTeam.setAvailableCredits(hiringTeam.getPaymentPlan().getActiveJobCount());
                            }
                            
                            hiringTeam.setPlanRenewedOn(invoiceDate);
                        }
                        
                        LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Setting hiring team with reset plan details  : ", hiringTeam);
                    }
                }
            }
            else if (event != null && event.getType().equals("customer.subscription.deleted")) 
            {
                Subscription    subscription = (Subscription) event.getData().getObject();
                
                LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Customer Subscription Deleted event received from stripe with Subscription  : ", subscription);

                if(subscription != null)
                {
                    HiringTeam[]    hiringTeams =   HiringTeam.SearchByAll().andStripeReference(new EqualsFilter<>(subscription.getCustomer())).search(objTran);

                    if(hiringTeams != null && hiringTeams.length > 0)
                    {
                        HiringTeam  hiringTeam  =   hiringTeams[0];

                        if(hiringTeam.getIsPPJ())
                        {
                            hiringTeam.setPaymentPlan(null);
                        }
                        
                        LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, "Setting hiring team with reset plan details  : ", hiringTeam);
                    }
                }
            }
        } 
        catch (IOException ex) 
        {
            LogMgr.log(LoggingArea.ALL, LogLevel.PROCESSING1, ex, "Error while handling webhook");
        }
    }
}
