<?php

namespace apexl\Io\modules\invoice\services;

use apexl\Config\Singleton;
use apexl\Io\includes\Hook;
use apexl\Io\includes\Routes;
use apexl\Io\includes\System;
use apexl\Io\modules\email\services\emailService;
use apexl\Io\modules\email\services\templateService;
use apexl\Io\modules\invoice\entities\invoiceEntity;
use apexl\Io\modules\invoice\entities\invoicePaymentEntity;
use apexl\Io\modules\invoice\entities\invoiceSubscriptionEntity;
use apexl\Io\modules\invoice\providers\xeroProvider;
use apexl\Io\modules\payment\entities\paymentEntity;
use apexl\Io\modules\payment\entities\userCreditEntity;
use apexl\Io\modules\payment\providers\stripeProvider;
use apexl\Io\modules\payment\services\cardValidationService;
use apexl\Io\modules\payment\services\paymentService;
use apexl\Io\modules\product\entities\productEntity;
use apexl\Io\modules\subscription\entities\subscriptionCreditEntity;
use apexl\Io\modules\subscription\entities\subscriptionEntity;
use apexl\Io\modules\subscription\services\subscriptionService;
use apexl\Io\modules\user\entities\userContactDetailsEntity;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\services\currentUser;
use apexl\Io\services\Database;
use apexl\Io\services\Logger;
use apexl\Vault\Vault;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

class invoiceService {

    protected $config;
    protected $providerContactData = [];
    protected $maxPaymentFailures = 3;  // If payment fails this many times, suspend the attached subscriptions
    protected $nextAttempts = [];

    public function __construct()
    {
        $this->config = Singleton::getInstance();
        $this->setProvider();

        if (!defined('REMOTE_INVOICE_REF')) define('REMOTE_INVOICE_REF', 'Dashboard Inv Ref #{ID}');
    }

    /**
     * Set provider class (xero etc.)
     * @return void
     */
    public function setProvider()
    {
        $this->provider = null;
        $provider = $this->config->app->invoicing->provider ?? null;

        switch ($provider){
            case 'xero':
                $this->provider = new xeroProvider($this);
                break;
        }
    }

    /**
     * Create an invoice.  If provider config is set, create an invoice in the provider service
     * @param $userId
     * @param $lineItems
     * @param $subscriptions
     * @return invoiceEntity
     * @throws \Exception
     */
    public function createInvoice($userId, $lineItems, $sendReceipt=false, $subscriptions=[], $paymentProvider=null, $remoteOrderId=null, $rollForwardSubscriptions=true, $billingEnabled=true, $brandingThemeId=null, $lookupContactBy='email', $sendEmail=FALSE,$lineAmountType='Exclusive', $contactData=null, $sendToProvider=true)
    {
        if (count($lineItems) > 0) {

            list($lineItems, $creditApplied) = $this->applyCreditToLineItems($userId, $lineItems);
            list($total, $vat) = $this->getTotalFromLineItems($lineItems);

        $invoice = new invoiceEntity();
            $invoice->user_id = $userId;
        $invoice->invoice_date = date('Y-m-d');
        $invoice->total = $total;
        $invoice->vat = $vat;
        $invoice->line_items = json_encode($lineItems);
            $invoice->provider = $this->config->app->invoicing->provider ?? null;
            if ($remoteOrderId) $invoice->remote_order_id = $remoteOrderId;
            $invoice->credit_applied = $creditApplied > 0 ? $creditApplied : 0;
            if (!$billingEnabled) $invoice->billing_enabled = 0;
            $invoice->send_receipt = $sendReceipt;
            $invoice->payment_provider = $paymentProvider;
            $invoice->send_to_provider = $sendToProvider ? 1 : 0;
            if (!$brandingThemeId && isset($this->config->app->invoicing->xero->branding_themes->default)) {
                $brandingThemeId = $this->config->app->invoicing->xero->branding_themes->default;
            }
            $brandingTheme = $brandingThemeId ?? $this->config->app->invoicing->xero->default_branding_theme;
            $invoice->provider_branding_id = $brandingTheme;
            $invoice->store();
            $invReference = str_replace('{ID}', $invoice->id, REMOTE_INVOICE_REF);

            // Roll forward subscriptions
            if (count($subscriptions) > 0 && $rollForwardSubscriptions) {
                $subscriptionService = new subscriptionService();
                $subscriptionService->rollForwardSubscriptions($subscriptions);
            }

            if ($creditApplied > 0) {
                $this->subtractCredit($userId, $creditApplied, "Credit applied to invoice #" . $invoice->id);
            }

            // Link subscriptions to invoice
            foreach ($subscriptions as $subscription) {
                $invoiceSubscriptionEntity = new invoiceSubscriptionEntity();
                $invoiceSubscriptionEntity->invoice_id = $invoice->id;
                $invoiceSubscriptionEntity->subscription_id = $subscription->id;
                $invoiceSubscriptionEntity->store();
            }

            if ($this->provider) {
                if (!$contactData) {
                    list($contactData, $lookupContactBy) = $this->getProviderContactDataByUser($userId, $lookupContactBy);
                }

                if ($sendToProvider) {
                    try {
                        list($invoiceId, $invoiceNumber, $contactData) = $this->createProviderInvoice($lineItems, $contactData, $lookupContactBy, null, '', $brandingTheme, $sendEmail, $invReference, 'ACCREC', 'GBP', $lineAmountType);

                    $this->providerContactData[$invoiceNumber] = $contactData;

                    $invoice->provider_id = $invoiceId;
                    $invoice->invoice_number = $invoiceNumber;
                    $invoice->store();
                    } catch (\Exception $e) {
                        $invoice->provider_error = $e->getMessage();
                        $invoice->on_hold = 1;
                        $invoice->store();

                        $this->providerErrorAlert($invoice);
                    }
                }
            }
        }


        return $invoice;
    }

    protected function getProviderContactDataByUser($userId, $lookupContactBy)
    {
        $userEntity = new userEntity();
        $userEntity->load($userId);
        $contactData = (object)[
            'name' => trim($userEntity->first_name . ' ' . $userEntity->last_name),
            'firstName' => $userEntity->first_name,
            'lastName' => $userEntity->last_name,
            'emailAddress' => $userEntity->email,
            'taxNumber' => ""
        ];

        $userContactDetails = new userContactDetailsEntity();
        $userContactDetails->load($userId);
        if (isset($userContactDetails->phone) && $userContactDetails->phone) {
            $contactData->phone = $userContactDetails->phone;
        }
        if (isset($userContactDetails->shipping_address_1) && ($userContactDetails->shipping_address_1 || $userContactDetails->billing_address_1)) {
            $addressType = $userContactDetails->same_address == 1 ? 'shipping' : 'billing';
            if ($userContactDetails->{$addressType.'_address_1'}) {
                $contactData->address = [
                    'AddressType'   => 'POBOX',
                    'AddressLine1'  => $userContactDetails->{$addressType.'_address_1'},
                    'AddressLine2'  => $userContactDetails->{$addressType.'_address_2'},
                    'AddressLine3'  => $userContactDetails->{$addressType.'_address_3'},
                    'City'          => $userContactDetails->{$addressType.'_town'},
                    'Region'        => $userContactDetails->{$addressType.'_county'},
                    'PostalCode'    => $userContactDetails->{$addressType.'_postcode'},
                    'Country'    => $userContactDetails->{$addressType.'_country'},
                ];
            }
        }

        if (isset($userEntity->invoice_name_override) && trim($userEntity->invoice_name_override) != "") {
            $contactData->name = $userEntity->invoice_name_override;
            $lookupContactBy = 'name';
        }

        return [$contactData, $lookupContactBy];
    }

    public function getProviderContactData($invoiceNumber)
    {
        return $this->providerContactData[$invoiceNumber] ?? null;
    }

    public function sendInvoiceToProvider($invoiceId, $providerName=null)
    {

        $result = (object)[
            'invoices_sent' => 0,
            'invoice_payments_sent' => 0,
        ];

        if ($this->provider) {
            $invoiceEntity = new invoiceEntity();
            $invoiceEntity->load($invoiceId);

            if ((isset($invoiceEntity->provider_id) || is_null($invoiceEntity->provider_id)) && trim($invoiceEntity->provider_id) == "") {

                $lineItems = json_decode($invoiceEntity->line_items);

                $userEntity = new userEntity();
                $userEntity->load($invoiceEntity->user_id);
                $lookupContactBy = $providerName ? 'name' : 'email';
                $contactData = (object)[
                    'name' => trim($providerName ?? $userEntity->first_name . ' ' . $userEntity->last_name),
                    'firstName' => $userEntity->first_name,
                    'lastName' => $userEntity->last_name,
                    'emailAddress' => $userEntity->email,
                    'taxNumber' => ""
                ];



                    $brandingTheme = $this->config->app->invoicing->xero->branding_themes->default ?? $this->config->app->invoicing->xero->default_branding_theme;
                //$brandingTheme = 'd68d8639-e9ba-4f7b-a603-a53ac6e3db38';

                if (isset($invoiceEntity->provider_branding_id) && trim($invoiceEntity->provider_branding_id) != "") {
                    $brandingTheme = trim($invoiceEntity->provider_branding_id);
                }

                list($invoiceId, $invoiceNumber, $contactData) = $this->createProviderInvoice($lineItems, $contactData, $lookupContactBy, null, '', $brandingTheme);

                $result->invoices_sent++;

                $invoiceEntity->provider_id = $invoiceId;
                $invoiceEntity->invoice_number = $invoiceNumber;
                $invoiceEntity->store();

                $invoicePaymentEntity = new invoicePaymentEntity();
                $payments = $invoicePaymentEntity->getPayments($invoiceEntity->id);


                foreach ($payments as $invoicePayment) {
                    $invoicePaymentEntity = new invoicePaymentEntity();
                    $invoicePaymentEntity->loadByPaymentId($invoicePayment->id);
                    if (trim($invoicePaymentEntity->provider_payment_id == "")) {
                        $paymentEntity = new paymentEntity();
                        $paymentEntity->load($invoicePaymentEntity->payment_id, true);
                        if ($paymentEntity->status == 'successful') {
                            $this->submitPaymentToProvider($paymentEntity, $invoiceEntity, $invoicePaymentEntity);
                            $result->invoice_payments_sent++;
                        }
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Create an invoice line item object for a given product ID
     * @param $name
     * @param $quantity
     * @param $productId
     * @param $total
     * @return void
     */
    public function createProductLineItem($name, $quantity, $productId, $total=null)
    {
        $productEntity = new productEntity();
        $productEntity->load($productId);
        if (isset($productEntity->id) && $productEntity->id) {
            $price = is_numeric($total) ? $total : $productEntity->price;
            $this->createInvoiceLineItem($name, $quantity, $productEntity->price, $productEntity->invoice_nominal_code, $productId);
        }
    }

    /**
     * Create an invoice line item
     * @param $name
     * @param $quantity
     * @param $perUnitCost
     * @param $nominalCode
     * @param $productId
     * @return array|object
     */
    public function createInvoiceLineItem($description, $quantity, $perUnitCost, $nominalCode, $productId=null, $taxType=null)
    {
        if ($quantity == 0) return [];
        $total = number_format($quantity*$perUnitCost, 2, '.', '');
        $totalRaw = round($quantity*$perUnitCost, 2);

        $overrideNominalCode = System::getVariable('override_invoice_nominal_code') ?? null;

        //$nominalCode = '200';
        $lineItem = (object)[
            'description' => $description,
            'accountCode' => $overrideNominalCode ? $overrideNominalCode : $nominalCode,
            'quantity' => $quantity,
            'perUnit' => !in_array($taxType, ['ECZROUTPUT', 'ZERORATEDOUTPUT']) ? $perUnitCost : number_format($perUnitCost, 2, '.', ''),
            'amount' => !in_array($taxType, ['ECZROUTPUT', 'ZERORATEDOUTPUT']) ? $total : number_format($totalRaw, 2, '.', ''),
            'vat' => !in_array($taxType, ['NONE', 'ECZROUTPUT', 'ZERORATEDOUTPUT']) ? number_format(0.2*$totalRaw, 2, '.', '') : 0,
            'productId' => $productId
        ];
        if ($taxType) {
            $lineItem->taxType = $taxType;
        }

        return $lineItem;
    }

    /**
     * Apply a user's existing credit to a group of line items
     * @param $userId
     * @param $lineItems
     * @return array
     */
    public function applyCreditToLineItems($userId, $lineItems)
    {
        $credit = $this->getTotalCredit($userId);
        $initialCredit = $credit;

        foreach ($lineItems as $id => $lineItem) {
            if ($credit > 0) {
                $totalBeforeCredit = $lineItem->amount;
                list($lineItem->amount, $credit) = $this->applyCreditToAmount($lineItem->amount, $credit);
                $lineItem->vat = round($lineItem->vat*($lineItem->amount/$totalBeforeCredit), 2); // reduce VAT accordingly
                $lineItem->creditApplied = round($totalBeforeCredit - $lineItem->amount, 2); // Note credit applied on lineItem
                $creditApplied = $lineItem->creditApplied;
                $credit -= $creditApplied;
                $lineItems[$id] = $lineItem;
            }
        }

        return [$lineItems, ($initialCredit - $credit)];
    }

    /**
     * Get total and total VAT from a group of line items
     * @param $lineItems
     * @return array
     */
    public function getTotalFromLineItems($lineItems)
    {
        $total = 0.00;
        $vat = 0.00;

        foreach ($lineItems as $id => $lineItem) {
            $total += $lineItem->amount;
            $vat += $lineItem->vat;
        }

        return [round($total,2), round($vat,2)];
    }

    /**
     * Function to handle automated creation of subscription invoices
     * @return Response
     * @throws \Exception
     */
    public function triggerSubscriptionInvoices(Request $request, Response $response, $args)
    {
        $pauseBilling = System::getVariable('io_invoice_pause_billing') ?? null;
        if ($pauseBilling == '1') return System::asJson($response, ['subscriptionsProcessed' => 0, 'reason' => 'billing paused by io_invoice_pause_billing variable']);

        if (date('H') < 8 || date('H') > 17) return System::asJson($response, ['subscriptionsProcessed' => 0, 'reason' => 'before 8am or after 6pm']);

        if (!self::checkStartCron()) return System::asJson($response, ['error' => 'exit due to cron duplication. process: '.getmypid()]);

        try {

        $subscriptionEntity = new subscriptionEntity();
        $productEntity = new productEntity();
        $subscriptionService = new subscriptionService();
        $toBill = $subscriptionEntity->getSubscriptionsToBill(date('Y-m-d'), 1, 24);
        $subscriptionsProcessed = 0;

        foreach ($toBill as $userId => $providerSubscriptions) {
            foreach ($providerSubscriptions as $providerName => $subscriptions) {

            $allLineItems = [];

            $subscriptionsBilled = [];
            $gracePeriodSubscriptions = [];

            foreach ($subscriptions as $subscription) {
                $lineItems = [];

                $incCharge = !$subscription->grace_period_ends || $subscription->next_billing_date >= $subscription->grace_period_ends; // If this is the grace period, subtract the base subscription charge
                if ($subscription->enabled == '0') $incCharge = false;
                if (!$incCharge) $gracePeriodSubscriptions[$subscription->id] = $subscription;

                $productEntity->load($subscription->product_id);
                if (isset($productEntity->id)) {
                    if ($incCharge) {// Get invoice line items
                    $billingDate = \DateTime::createFromFormat('Y-m-d', $subscription->next_billing_date);

                            $price = $productEntity->price;
                            $quantity = $subscription->product_quantity;
                            if (trim($subscription->price_override) != "" && $subscription->price_override > 0) {
                                $quantity = 1;
                                $price = trim($subscription->price_override);
                            }

                            $nominalCode = $productEntity->invoice_nominal_code;
                            if (trim($subscription->nominal_code_override) != "") $nominalCode = $subscription->nominal_code_override;

                            $taxType = null;
                            if (trim($subscription->tax_type_override) != "") $taxType = trim($subscription->tax_type_override);

                            $lineItems[] = $this->createInvoiceLineItem("Subscription Fee: {$productEntity->product_name}, scheduled billing date: {$billingDate->format('d M Y')}", $quantity, $price, $nominalCode, $subscription->product_id, $taxType);
                    $subscriptionsBilled[$subscription->id] = $subscription;
                    }
                    $newLineItems = Hook::processHook('trigger_subscription_invoice_line_items', $subscription, $lineItems);
                    if (is_array($newLineItems)) $lineItems = $newLineItems;
                }

                $subscriptionsProcessed++;
                $allLineItems = array_merge($allLineItems, $lineItems);
            }
                //echo '<pre>';
                //print_r($allLineItems);
                //echo '</pre>';
                //die();
            if (count($allLineItems) > 0) { // Create the invoice
                if (!self::checkContinueCron()) return System::asJson($response, ['error' => 'exit due to cron duplication. process: '.getmypid()]);
                    Hook::processHook('pre_raise_subscription_invoice', $subscriptions);
                    $brandingTheme = $this->config->app->invoicing->xero->branding_themes->{$providerName} ?? null;
                    $invoice = $this->createInvoice($userId, $allLineItems, $this->config->app->invoicing->send_receipts ?? false, $subscriptionsBilled, $providerName, null, true, $providerName=='stripe',$brandingTheme);
                if (isset($invoice->id) && $invoice->id) $allLineItems = Hook::processHook('confirm_subscription_invoice_line_items', $invoice, $allLineItems, $userId);
            }

            // Roll forward subscription to next billing period
            $subscriptionService->rollForwardSubscriptions($gracePeriodSubscriptions);

            foreach ($subscriptions as $subscription) {
                $subscriptionEntity->load($subscription->id);
                $subscriptionEntity->last_checked = date('Y-m-d H:i:s');
                $subscriptionEntity->store();
            }
        }
        }
        } catch (\Exception $e) {
            $this->genericAlert('URGENT - GlobalM2MSIM subscription billing error', "There has been an error with the subscription invoicing process:<br><br> {$e->getMessage()}");
        }

        return System::asJson($response, ['subscriptionsProcessed' => $subscriptionsProcessed]);
    }

    /**
     * Function to handle automated billing of invoices via payments provider
     * @return Response
     * @throws \Exception
     */
    public function triggerInvoicePayments(Request $request, Response $response, $args)
    {
        if (!self::checkStartCron('invoice_payment')) return System::asJson($response, ['error' => 'exit due to cron duplication. process: '.getmypid()]);

        try {

        $invoiceEntity = new invoiceEntity();
        $invoices = $invoiceEntity->getChargeable(1);
        $cardValidationService = new cardValidationService();

        $cutoff = new \DateTime('now');
        $timeInterval = 24*60; // Don't attempt to charge a user more than once per day
        $cutoff->sub(new \DateInterval("PT".$timeInterval."M"));
        $result = (object)['invoice_charge_attempts' => 0, 'invoice_charge_passes' => 0];

        foreach ($invoices as $invoice) {

            $invoiceEntity = new invoiceEntity();
            $invoiceEntity->load($invoice->id);
            $ref = 'InvId-' . $invoiceEntity->id;

            if (!$this->invoiceSafetyNetChecks($invoiceEntity, $ref)) { // Perform safety net checks
            $paymentEntity = new paymentEntity();
                $paymentEntity->getMostRecentForUser($invoice->user_id, [] /*['failed', 'three_d_secure_required']*/);
            $lastPaymentForUser = \DateTime::createFromFormat('U', $paymentEntity->created); // Get the last payment attempt for this user

            if ($lastPaymentForUser < $cutoff) {
                    if (!self::checkContinueCron('invoice_payment')) return System::asJson($response, ['error' => 'exit due to cron duplication. process: '.getmypid()]);
                    $this->attemptInvoicePayment($invoiceEntity, $ref, true, $this->getPaymentNotesFromInvoice($invoiceEntity));
                $result->invoice_charge_attempts++;

                } else {
                    $result->invoice_charge_passes++;
                }
            }
            $invoiceEntity->last_payment_attempt = date('Y-m-d H:i:s'); // Set last payment attempt date and time
                $invoiceEntity->store();
        }
        } catch (\Exception $e) {
            $this->genericAlert('URGENT - GlobalM2MSIM invoice payments error', "There has been an error with the invoice payment triggering process:<br><br> {$e->getMessage()}");
        }

        return $result;
    }

    public function triggerProviderUploads(Request $request, Response $response, $args)
    {
        if (!self::checkStartCron('provider_uploads')) return System::asJson($response, ['error' => 'exit due to cron duplication. process: '.getmypid()]);

        try {
            $invoiceService = new invoiceService();
            $invoiceEntity = new invoiceEntity();
            $invoices = $invoiceEntity->getRequiringProviderUpload(1);
            foreach ($invoices as $invoice) {
                if ($invoice->send_to_provider == '1' && trim($invoice->provider_id) == '') {
                    if (!self::checkContinueCron('provider_uploads')) return System::asJson($response, ['error' => 'exit due to cron duplication. process: ' . getmypid()]);
                    $invoiceService->sendInvoiceToProvider($invoice->id);
                }
            }
        } catch (\Exception $e) {
            $this->genericAlert('URGENT - GlobalM2MSIM provider invoice uploads error', "There has been an error with the invoice provider upload triggering process:<br><br> {$e->getMessage()}");
        }

        return true;
    }

    public function triggerPaymentAlerts(Request $request, Response $response, $args)
    {

        $checks = [
            (object)[
                'varName' => 'cron_subscription_billing_completed',
                'triggerDays' => '2',
                'alert' => 'Cron Subscription Billing Invoice Generation failed to complete for more than two daya'
            ],
            (object)[
                'varName' => 'cron_invoice_payments_completed',
                'triggerDays' => '2',
                'alert' => 'Cron Invoice Charging failed to complete for more than two daya'
            ],
            (object)[
                'varName' => 'cron_provider_uploads_completed',
                'triggerDays' => '2',
                'alert' => 'Cron automated provider invoice uploads failed to complete for more than two daya'
            ]
        ];

        $alerts = [];
        foreach ($checks as $check) {
            $lastSuccessfulRun = System::getVariable($check->varName);
            if ($lastSuccessfulRun) {
                $lastSuccessfulRun = \DateTime::createFromFormat('U', $lastSuccessfulRun);
                $lastSuccessfulRun->modify("+{$check->triggerDays} days");
                $now = new \DateTime('now');
                if ($lastSuccessfulRun < $now) {
                    $alerts[] = $check->alert;
                }
            }
        }

        $lastAlertEmail = System::getVariable('payment_alert_email_sent');
        if ($lastAlertEmail) {
            $lastSent = \DateTime::createFromFormat('U', $lastAlertEmail);
        }
        if (count($alerts) > 0 && (!$lastAlertEmail || ((int)$lastAlertEmail+84600) < time())) {
            $emailData = [
                'subject' => 'URGENT: GlobalM2MSIM Dashboard Cron failures',
                'message' => '<p>'.implode("<br />", $alerts).'</p>',
            ];

            $emailService = new emailService();
            $result = $emailService->send(
                (object) [
                    'to' => "[WEBMASTER]",
                    'subject' => 'URGENT: GlobalM2MSIM Dashboard Cron failures',
                    'body' => templateService::fetch("generic", $emailData),
                    'user_id' => 0,
                ],
                'generic'
            );

            System::setVariable('payment_alert_email_sent', date('U'));

            Logger::log('payment_alert_email_sent', ['sent' => $result, 'alerts' => $alerts]);
        }
    }

    protected function providerErrorAlert($invoiceEntity)
    {
        $userEntity = new userEntity();
        $userEntity->load($invoiceEntity->user_id);

        $emailData = [
            'subject' => 'High Priority: GlobalM2MSIM Dashboard Invoice not sent to Provider',
            'message' => "<p>Invoice Local ID: {$invoiceEntity->id} could not be sent to Xero.<br><br>Customer name: {$userEntity->getNiceName()}.</p><p>We need to investigate the reason for this, and ensure the invoice is billed correctly.</p>",
        ];

        $emailService = new emailService();
        $result = $emailService->send(
            (object) [
                'to' => "[WEBMASTER]",
                'subject' => 'High Priority: GlobalM2MSIM Dashboard Invoice not sent to Provider',
                'body' => templateService::fetch("generic", $emailData),
                'user_id' => 0,
            ],
            'generic'
        );
    }

    protected function genericAlert($subject, $message)
    {
        $emailData = [
            'subject' => $subject,
            'message' => "<p>$message</p>",
        ];

        $emailService = new emailService();
        $result = $emailService->send(
            (object) [
                'to' => "[WEBMASTER]",
                'subject' => $subject,
                'body' => templateService::fetch("generic", $emailData),
                'user_id' => 0,
            ],
            'generic'
        );
    }

    protected static function checkStartCron($process='subscription')
    {
        $lastStarted = System::getVariable($process.'_cron_process_started');
        if (!$lastStarted || microtime(true) - (float)$lastStarted > 60) {
            // continue if no other process started in the last minute
            System::setVariable($process.'_cron_process_started', microtime(true));
            System::setVariable($process.'_cron_process_pid', getmypid());
            sleep(3);
            return self::checkContinueCron($process);
        }

        return false;
    }

    protected static function checkContinueCron($process='subscription')
    {
        $activePid = System::getVariable($process.'_cron_process_pid');
        if ($activePid == getmypid()) {
            return true;
        }

        return false;
    }

    /**
     * This function provides a safety net against inadvertant billing of the same invoice multiple times
     * @param $invoiceEntity
     * @param $ref
     * @return bool
     */
    protected function invoiceSafetyNetChecks($invoiceEntity, $ref)
    {

        // 1. Check invoice payments
        $totalPaid = 0.0;
        $paymentEntity = new paymentEntity();
        $payments = $paymentEntity->fetchAllByRef($ref);
        foreach ($payments as $payment) {
            if ($payment->status == 'successful') {
                $totalPaid += (float)$payment->amount;
            }
        }
        $invTotal = (float)($invoiceEntity->total + $invoiceEntity->vat);
        if ($invTotal - $totalPaid <= 0.015) {
            $invoiceEntity->paid = 1;
            $invoiceEntity->store();
            if ($invoiceEntity->send_receipt) {
                $this->sendReceipt($invoiceEntity);
            }
            return true;
        }

        // 2. Check balance in Xero
        if ($this->downloadProviderPaidStatus($invoiceEntity)) return true;

        return false;

    }

    public function backfillProviderPaidStatus()
    {
        $vault = Vault::getInstance();
        $invoice = $vault
            ->select('invoice')
            ->fields()
            ->where('paid', 0)
            ->where('payment_provider', 'gocardless')
            ->where('provider_id', NULL, 'IS NOT NULL')
            ->where('paid_status_check', NULL, 'IS NULL')
            ->orderBy('invoice_date', 'DESC')
            ->limit(1)
            ->execute()
            ->fetchAssoc();

        if (isset($invoice['provider_id'])) {
            $invoiceEntity = new invoiceEntity();
            $invoiceEntity->load($invoice['id']);
            try {
                $this->downloadProviderPaidStatus($invoiceEntity);
            } catch (\Exception $e) {
                $invoiceEntity->paid_status_check_error = $e->getMessage();
            }
            $invoiceEntity->paid_status_check = date('Y-m-d H:i:s');
            $invoiceEntity->store();
        }
    }

    protected function downloadProviderPaidStatus($invoiceEntity)
    {
        if (isset($invoiceEntity->provider_id) && $invoiceEntity->provider_id) { // Submit payment information to provider
            $invoice = $this->provider->getInvoice($invoiceEntity->provider_id);
            if ($invoice->AmountDue < 0.015) {
                $invoiceEntity->paid = 1;
                $invoiceEntity->store();
                if ($invoiceEntity->send_receipt) {
                    $this->sendReceipt($invoiceEntity);
                }
                return true;
            }
        }

        return false;

    }

    public function getPaymentNotesFromInvoice($invoiceEntity)
    {
        $user = new userEntity();
        $user->load($invoiceEntity->user_id);
        $lineItems = json_decode($invoiceEntity->line_items);
        foreach ($lineItems as $id => $item) {
            $lineItems[$id]->email = $user->email;
        }

        if(strlen(json_encode($lineItems)) > 999){
            $lineItems = [];
            $lineItems[] = 'Data Too long for stripe - Can be retrieved from dashboard if needed.';
        }

        return $lineItems;
}
    /**
     * Attempt to chrage payment for a single invoice
     * @param $invoiceEntity
     * @param $ref
     * @param $isSubscriptionPayment
     * @param $data
     * @param $meta
     * @param $userPresent
     * @return paymentEntity|null
     */
    public function attemptInvoicePayment($invoiceEntity, $ref="", $isSubscriptionPayment=true, $data=[], $meta=null, $userPresent=false, $sendPaymentInfo=true)
    {
        $paymentEntity = null;

        if ($invoiceEntity->billing_enabled == '1') {

            $paymentService = new paymentService();
            $cardValidationService = new cardValidationService();
            $subscriptionEntity = new subscriptionEntity();
            $subscriptions = $subscriptionEntity->getByInvoiceId($invoiceEntity->id);
            $cardValidationService->setSubscriptions($subscriptions);
            $billingToken = $paymentService->getBillingToken($invoiceEntity->user_id, $invoiceEntity->payment_provider);
                $userEntity = new userEntity();
                $userEntity->load($invoiceEntity->user_id);

            if (!$billingToken) { // No billable token found - place invoice on hold
                $this->processSubscriptionInvoicePaymentFailure($invoiceEntity);
            } else {
                $transactionData = [];
                if ($invoiceEntity->transaction_data) $transactionData = json_decode($invoiceEntity->transaction_data);
                $paymentEntity = $cardValidationService->capturePaymentWithToken($invoiceEntity->user_id, $invoiceEntity->payment_provider, ($invoiceEntity->total + $invoiceEntity->vat), $ref, $isSubscriptionPayment, $data, $meta, $transactionData, $userPresent);
                if (isset($paymentEntity->id)) {

                    $invoicePaymentEntity = $this->addPaymentToInvoice($invoiceEntity->id, $paymentEntity);
                    // Submit payment to Xero, or other provider
                    if ($sendPaymentInfo && trim($invoiceEntity->provider_id) != "") {
                    if (trim($invoiceEntity->provider_id) != "") {
                        if ($paymentEntity->status == 'successful') {
                            $this->submitPaymentToProvider($paymentEntity, $invoiceEntity, $invoicePaymentEntity);
                        }
                    }
                }

                    $this->checkStoreFailedAttempts($invoiceEntity, $paymentEntity);

                    if (isset($paymentEntity->status) && $paymentEntity->status == 'successful') {
                        $this->reenableInvoiceSubscriptions($invoiceEntity, (!isset($this->config->app->payments->subscriptions->disableOnCreation) || $this->config->app->payments->subscriptions->disableOnCreation == false));
                    }
                }
            }

            $this->checkAndMarkAsPaid($invoiceEntity);
        }

        return $paymentEntity;
    }

    public function checkStoreFailedAttempts($invoiceEntity, $paymentEntity)
    {
        // handle failed payment attempts
        if (isset($paymentEntity->status) && $paymentEntity->status == 'failed') {
            $invoiceEntity->failed_payment_attempts = $invoiceEntity->failed_payment_attempts + 1;
            $invoiceEntity->store();
            if ($invoiceEntity->failed_payment_attempts >= $this->maxPaymentFailures) {
                $this->processSubscriptionInvoicePaymentFailure($invoiceEntity);
            }
        }
    }

    public function reenableInvoiceSubscriptions($invoiceEntity, $reEnableSubscriptions=true)
    {
        Logger::log('reenable_invoice_subscriptions_start', ['invoice_id' => $invoiceEntity->id, 'reEnableSubscriptions' => $reEnableSubscriptions]);

        $subscriptionEntity = new subscriptionEntity();
        $subscriptionService = new subscriptionService();
        $subscriptions = $subscriptionEntity->getByInvoiceId($invoiceEntity->id);

        Logger::log('reenable_invoice_subscriptions_data', ['invoice_id' => $invoiceEntity->id, 'reEnableSubscriptions' => $reEnableSubscriptions, 'subscriptions' => $subscriptions]);

        if (($reEnableSubscriptions) && is_array($subscriptions)) {
            foreach ($subscriptions as $subscription) {
                Logger::log('reenable_invoice_subscriptions_in_loop', ['invoice_id' => $invoiceEntity->id, 'subscription' => $subscription]);
                if ($subscription->enabled == '1' && trim($subscription->cancellation_date) != ""
                    || ($subscription->enabled == 0 && trim($subscription->cancellation_date) == "" && $invoiceEntity->abandoned != 1)) {
                    Logger::log('reenable_invoice_subscriptions_in_if', ['invoice_id' => $invoiceEntity->id, 'subscription' => $subscription]);
                    $subscriptionEntity = new subscriptionEntity();
                    $subscriptionEntity->load($subscription->id);
                    $subscriptionEntity->cancellation_date = null;
                    $subscriptionEntity->store();
                    Logger::log('reenable_invoice_subscriptions_stage1_finished', ['invoice_id' => $invoiceEntity->id, 'subscriptionEntity' => $subscriptionEntity->getData()]);

                    if ($subscription->enabled == 0) {
                        Logger::log('reenable_invoice_subscriptions_stage2_start', ['invoice_id' => $invoiceEntity->id, 'subscriptionEntity' => $subscriptionEntity->getData()]);
                        $startDate = \DateTime::createFromFormat('Y-m-d', $subscription->date_starts);
                        $now = new \DateTime('now');
                        $interval = $startDate->diff($now);
                        if ($interval->format('d') <= 7) {
                            $subscriptionEntity->enabled = 1;
                            $subscriptionEntity->store();
                            Logger::log('reenable_invoice_subscriptions_stage2_finished', ['invoice_id' => $invoiceEntity->id, 'subscriptionEntity' => $subscriptionEntity->getData()]);
                        }
                    }
                    Logger::log('reenable_invoice_subscriptions_finished', ['invoice_id' => $invoiceEntity->id, 'subscriptionEntity' => $subscriptionEntity->getData()]);
                    Hook::processHook('subscription_enabled', $subscriptionEntity, "Subscription re-enabled due to successful payment for ".$invoiceEntity->invoice_number);
                }
            }
        }
    }

    public function processSubscriptionInvoicePaymentFailure($invoiceEntity)
    {
        $invoiceEntity->on_hold = 1;
        $invoiceEntity->store();
        $subscriptionEntity = new subscriptionEntity();
        $subscriptionService = new subscriptionService();
        $subscriptions = $subscriptionEntity->getByInvoiceId($invoiceEntity->id);
        $cancellationDate = new \DateTime('now');
        $cancellationDate->add(new \DateInterval('P2M')); // Bill SIMs for a further 2 months
        $subscriptionIds = [];
        foreach ($subscriptions as $subscription) {
            $subscriptionIds[] = $subscription->id;
            $subscriptionEntity = new subscriptionEntity();
            $subscriptionEntity->load($subscription->id);
            $subscriptionEntity = $subscriptionService->suspendSubscription($subscriptionEntity, 'payment_failure_suspension', $cancellationDate->format('Y-m-d'), "Failed to take payment for invoice ". $invoiceEntity->invoice_number);
        }
        $userEntity = new userEntity();
        $userEntity->load($invoiceEntity->user_id);
        $this->paymentMethodFailureNotification($userEntity, $subscriptionIds);
    }

    protected function paymentMethodFailureNotification(userEntity $user, $subscriptionIds=[])
    {
        $from = $this->config->app->site->email_address ?? 'no-reply@localhost.com';
        $fromName = $this->config->app->site->name ?? 'localhost';
        $backEndDomain = $this->config->app->site->backend_domain ?? 'localhost';

        $sendable = true;
        if ($user->last_payment_failed_email) {
            $lastSent = \DateTime::createFromFormat('Y-m-d H:i:s', $user->last_payment_failed_email);
            $now = new \DateTime('now');
            $interval = $lastSent->diff($now);
            $daysDiff = (int)($interval->format('%a', true));
            if ($daysDiff < 3) {
                $sendable = false;
            }
        }

        if ($user->id && $sendable) {
            $args = [
                'userEmail' => $user->email,
                'userFirstName' => $user->first_name,
                'userLastName' => $user->last_name,
                'create_subscription' => 0,
                'addPaymentMethod' => 1,
                'total' => 0,
            ];
            $emailData = [
                'payment_link' => rtrim($this->config->app->site->backend_domain, "/").'/api/v1/payment/action/payment/validation?args='.base64_encode(json_encode($args)),
                'name' => $user->first_name
            ];
            $customData = Hook::processHook('pre_send_payment_failure_notification', $subscriptionIds);
            if (is_array($customData)) $emailData = array_merge($customData, $emailData);

            $emailService = new emailService();
            $result = $emailService->send(
                (object) [
                    'to' => $user->email,
                    'subject' => 'Payment Failure Notification',
                    'body' => templateService::fetch("payment_failure", $emailData),
                    'user_id' => $user->id,
                ],
                'payment_failure'
            );

            Logger::log('payment_failure_notification', ['sent' => $result, 'user' => $user->getData()]);

        }
    }

    public function reenableUserInvoices($userId)
    {
        $invoiceEntity = new invoiceEntity();
        $invoices = $invoiceEntity->getForUser($userId);
        foreach ($invoices as $invoice) {
            if ($invoice->on_hold == 1) {
                $invoiceEntity->load($invoice->id);
                $invoiceEntity->on_hold = 0;
                $invoiceEntity->failed_payment_attempts = 0;
                $invoiceEntity->store();
            }
        }
    }

    public function reenableSubscriptionInvoices($subscriptionId)
    {
        $invoiceEntity = new invoiceEntity();
        $invoices = $invoiceEntity->getForSubscription($subscriptionId);
        foreach ($invoices as $invoice) {
            if ($invoice->on_hold == 1) {
                $invoiceEntity->load($invoice->id);
                $invoiceEntity->on_hold = 0;
                $invoiceEntity->failed_payment_attempts = 0;
                $invoiceEntity->store();
            }
        }
    }

    /**
     * Submit payment to invoice provider and store the ID
     * @param $paymentEntity
     * @param $invoiceEntity
     * @param $invoicePaymentEntity
     * @return invoicePaymentEntity|void
     * @throws \Exception
     */
    public function submitPaymentToProvider($paymentEntity, $invoiceEntity=null, $invoicePaymentEntity=null)
    {
        if (!$invoicePaymentEntity) {
            $invoicePaymentEntity = new invoicePaymentEntity();
            $invoicePaymentEntity->loadByPaymentId($paymentEntity->id);
            if (!isset($invoicePaymentEntity->id)) return null;
        }

        if (!$invoiceEntity) {
            $invoiceEntity = new invoiceEntity();
            $invoiceEntity->load($invoicePaymentEntity->invoice_id);
        }
        if (isset($invoiceEntity->provider_id) && $invoiceEntity->provider_id) { // Submit payment information to provider
            if (!isset($this->config->app->invoicing->xero->dont_send_payments) || !in_array($invoiceEntity->payment_provider, $this->config->app->invoicing->xero->dont_send_payments)) {
                $providerPaymentId = $this->provider->createPayment($invoiceEntity->provider_id, $paymentEntity->amount, $paymentEntity->provider_ref_2, $this->config->app->invoicing->sale_account_code);
                $invoicePaymentEntity->provider_payment_id = $providerPaymentId;
                $invoicePaymentEntity->store();
            }
        }
    }

    /**
     * log a payment against an invoice
     * @param $invoiceId
     * @param $paymentEntity
     * @return void
     * @throws \Exception
     */
    public function addPaymentToInvoice($invoiceId, $paymentEntity)
    {
        $invoicePaymentEntity = new invoicePaymentEntity();
        $invoicePaymentEntity->invoice_id = $invoiceId;
        $invoicePaymentEntity->payment_id = $paymentEntity->id;
        $invoicePaymentEntity->store();

        return $invoicePaymentEntity;
    }

    /**
     * Check whether an invoice has been paid and mark it as such
     * @param $invoiceEntity
     * @return void
     */
    public function checkAndMarkAsPaid($invoiceEntity)
    {
        if (isset($invoiceEntity->id) && $invoiceEntity->id) {
            $invoicePaymentEntity = new invoicePaymentEntity();
            $payments = $invoicePaymentEntity->getPayments($invoiceEntity->id);

            //echo 'NUM Payments:' . count($payments);

            $totalPaid = 0.0;
            $chargeIds = [];
            foreach ($payments as $payment) {
                if ($payment->status == 'successful') {
                    $totalPaid += $payment->amount;
                    $chargeIds[] = $payment->provider_ref_2;
                }
            }

            $totalPaid = (float)$totalPaid;
            $invTotal = (float)($invoiceEntity->total + $invoiceEntity->vat);

            //echo "\nTot Due: " . $invTotal;
            //echo "\nTot Paid: " . $totalPaid;
            //echo "\nDifference: " . ($invTotal - $totalPaid);

            if ($invTotal - $totalPaid <= 0.001) {
                $invoiceEntity->paid = 1;
                $invoiceEntity->store();
                if ($invoiceEntity->send_receipt) {
                    $this->sendReceipt($invoiceEntity);
                }
                Hook::processHook('invoice_marked_as_paid', $invoiceEntity);
                Hook::processHook('on_successful_invoice_payment', $invoiceEntity, $chargeIds);
            }
        }
    }

    protected function sendReceipt($invoiceEntity)
    {
        if (!$invoiceEntity->receipt_sent_date) {

            $userEntity = new userEntity();
            $userEntity->load($invoiceEntity->user_id);
            if (isset($userEntity->id) && $userEntity->id > 0) {

                $from = $this->config->app->site->email_address ?? 'no-reply@localhost.com';
                $fromName = $this->config->app->site->name ?? 'localhost';
                $frontEndDomain = $this->config->app->site->frontend_domain ?? 'localhost';
                $link = rtrim($frontEndDomain, '/');// . '/sim-activation/verify-email/' . $session;

                $emailData = [
                    'first_name' => $userEntity->first_name,
                    'last_name' => $userEntity->last_name,
                    'email' => $userEntity->email,
                    'lineItems' => json_decode($invoiceEntity->line_items),
                    'subtotal' => number_format($invoiceEntity->total, 2),
                    'vat' => number_format($invoiceEntity->vat, 2),
                    'total' => number_format($invoiceEntity->total + $invoiceEntity->vat, 2),
                ];

                // Attach invoice
                $attachments = [];
                if ($this->provider) {
                    if ($content = $this->provider->getInvoicePDF($invoiceEntity->provider_id)){
                        $attachments[] = (object)[
                            'content' => $content,
                            'filename' => $invoiceEntity->invoice_number . '.pdf'
                        ];
                    }
                }

                $emailService = new emailService();
                $result = $emailService->send(
                    (object) [
                        'to' => $userEntity->email,
                        'subject' => 'GeoSIM - Your Receipt',
                        'body' => templateService::fetch("receipt_email", $emailData),
                        'user_id' => $userEntity->id,
                        'attachments' => $attachments,
                    ],
                    'receipt_email'
                );


                if ($result) {
                    $invoiceEntity->receipt_sent_date = date('Y-m-d H:i:s');
                    $invoiceEntity->store();
                }
                Logger::log('send_invoice_receipt', ['sent' => $result, 'invoice' => $invoiceEntity->getData()]);
            }

        }
    }

    /**
     * Update a users total credit
     * @param $userId
     * @param $newCreditAmount
     * @param $data
     * @return void
     * @throws \Exception
     */
    public function updateTotalCredit($userId, $newCreditAmount, $data)
    {
        // Delete existing credits for this user
        $userCreditEntity = new userCreditEntity();
        $credits = $userCreditEntity->getByUser($userId);
        foreach ($credits as $credit) {
            $userCreditEntity->load($credit->id);
            $userCreditEntity->delete();
        }

        $this->addCredit($userId, $newCreditAmount, $data);
    }

    /**
     * Add credit for a user
     * @param $userId
     * @param $newCreditAmount
     * @param $data
     * @return void
     * @throws \Exception
     */
    public function addCredit($userId, $newCreditAmount, $data)
    {
        // Add credit
        $userCreditEntity = new userCreditEntity();
        $userCreditEntity->user_id = $userId;
        $userCreditEntity->credit_amount = $newCreditAmount;
        $userCreditEntity->data = json_encode($data);
        $userCreditEntity->store();
    }

    /**
     * Subtract credit from a user's credit balance
     * @param $userId
     * @param $subtractAmount
     * @param $reason
     * @return void
     * @throws \Exception
     */
    public function subtractCredit($userId, $subtractAmount, $reason)
    {
        $totalCredit = $this->getTotalCredit($userId);
        $this->updateTotalCredit($userId, ((float)$totalCredit - (float)$subtractAmount), ['reason' => $reason]);
    }

    /**
     * OLD - FOR REF ONLY:   NOTE THIS FUNCTION ASSUMES STORED CREDIT VALUES EXCLUDE VAT
     * AND THAT $AMOUNT INCLUDES VAT
     * @param $amount
     * @param $userId
     * @return array
     */
    /* public function applyCreditToAmount($amount, $userId)
     {
         $totalCredit = $this->getTotalCredit($userId);
         $totalCreditWithVAT = $totalCredit*1.2;
         if ($totalCreditWithVAT >= $amount) {
             return [0, $totalCreditWithVAT-round($amount/1.2,2)];
         } else {
             return [($amount - $totalCreditWithVAT), $totalCredit];
         }
     }*/

    /**
     * Apply credit to a total and return the new total and remaining credit
     * @param $amount
     * @param $totalCredit
     * @return array
     */
    public function applyCreditToAmount($amount, $totalCredit)
    {
        if ($totalCredit >= $amount) {
            return [0, $amount];
        } else {
            return [($amount - $totalCredit), $totalCredit];
        }
    }

    /**
     * Get a user's credit balance
     * @param $userId
     * @return float
     */
    public function getTotalCredit($userId)
    {
        $userCreditEntity = new userCreditEntity();
        $credits = $userCreditEntity->getByUser($userId);
        $totalCredit = 0.0;
        foreach ($credits as $credit) {
            $totalCredit += $credit->credit_amount;
        }

        return $totalCredit;
    }

    /**
     * Find an invoice entity using a provider ID (e.g. Xero ID)
     * @param $providerId
     * @return invoiceEntity|null
     */
    public function getInvoiceByProviderId($providerId)
    {
        $invoiceEntity = new invoiceEntity();
        $invoiceEntity->loadByProviderId($providerId);
        if (isset($invoiceEntity) && $invoiceEntity->provider_id == $providerId) {
            return $invoiceEntity;
        }

        return null;
    }

    public function getInvoiceByReference($reference)
    {
        if (str_replace("{ID}", "", REMOTE_INVOICE_REF) == preg_replace('/[0-9]+/', '', $reference)) {
            $invoiceId = preg_replace("/[^0-9]/", "", $reference );
            if (is_numeric($invoiceId)) {
                $invoiceEntity = new invoiceEntity();
                $invoiceEntity->load($invoiceId);
                return $invoiceEntity;
            }
        }

        return null;
    }

    public function getInvoiceByDashboardInvoiceRef($providerId)
    {
        $invoiceEntity = new invoiceEntity();
        $invoiceEntity->loadByProviderId($providerId);
        if (isset($invoiceEntity) && $invoiceEntity->provider_id == $providerId) {
            return $invoiceEntity;
        }

        return null;
    }

    public function getProviderContactById($id)
    {
        if ($this->provider) {
            return $this->provider->getContact((object)['id' => $id], 'id');
        }
    }

    /**
     * Get provider invoices for a given email address
     * @param $email
     * @return void
     */
    public function getProviderInvoices($email, $expanded=false)
    {
        if ($this->provider) {
            return $this->provider->getInvoices($email, $expanded);
        }
    }

    /**
     * Create an invoice in the provider service (e.g. on Xero)
     * @param $lineitems
     * @param $contactData
     * @param $dueDate
     * @param $desc
     * @param $branding
     * @param $sendEmail
     * @param $type
     * @param $currency
     * @return void
     */
    public function createProviderInvoice($lineitems, $contactData, $lookupContactBy='name', $dueDate = null, $desc = '', $branding = '5540a201-bccd-4585-a66d-c5d2aa40cc56', $sendEmail=FALSE, $reference=null, $type = 'ACCREC', $currency = 'GBP', $lineAmountType='Exclusive')
    {
        if ($this->provider) {
            return $this->provider->createInvoice($lineitems, $contactData, $lookupContactBy, $lineAmountType, $dueDate, $desc, $branding, $sendEmail, $reference, $type, $currency);
        }
    }

    public function creditProviderInvoice($invoiceEntity)
    {
        if ($this->provider && $invoiceEntity->provider_id) {
            list($contactData, $lookupContactBy) = $this->getProviderContactDataByUser($invoiceEntity->user_id, 'email');
            return $this->provider->creditInvoice($invoiceEntity->provider_id, $contactData, $lookupContactBy);
        }

        return null;
    }

    public function createProviderContact($userId, $invoiceName=null)
    {

        list($localContactData, $lookupContactBy) = $this->getProviderContactDataByUser($userId, 'email');
        if ($invoiceName) $localContactData->name = $invoiceName;

        $addContact = TRUE;
        $xeroContact = $this->provider->getContact($localContactData, $lookupContactBy);

        // check existing / create new contact
        foreach($xeroContact as $contact){
            if(!empty($contact)) {
                $addContact = FALSE; // Contact found - don't add a new one
                break;
            }
        }
        if($addContact) $contact = $this->provider->createContact($localContactData); //  create a new contact

        return $addContact;
    }


    public function getInvoiceUrl($invoiceId)
    {

        $idOnlyNumbers = preg_replace("/[^0-9]/", "", $invoiceId );
        if (strlen($invoiceId) == strlen($idOnlyNumbers)) {
            $invoiceEntity = new invoiceEntity();
            $invoiceEntity->load($invoiceId);
            if (isset($invoiceEntity->provider_id)) {
                return $this->provider->getInvoiceUrl($invoiceEntity->provider_id);
            }
        }

        return $this->provider->getInvoiceUrl($invoiceId);
    }

    /**
     * Get public URL of provider invoice
     * @param $providerInvoiceId
     * @return void
     */
    public function getProviderInvoiceUrl($providerInvoiceId)
    {
        if ($this->provider) {
            return $this->provider->getInvoiceUrl($providerInvoiceId);
        }
    }

    /**
     * Get provider branding themes
     * @return void
     */
    public function getProviderBrandingThemes()
    {
        if ($this->provider) {
            return $this->provider->getBrandingThemes();
        }
    }

    /**
     * Get provider accounts
     * @return void
     */
    public function getProviderAccounts()
    {
        if ($this->provider) {
            $this->provider->getAccounts();
        }
    }

    public function getContact()
    {
        if ($this->provider) {
            $result = $this->provider->getContact((object)[]);
            print_r($result);
            die();
        }
    }

    public function getProviderContactHistory($identifer, $type)
    {
        if ($this->provider) {
            $this->provider->getContactHistory($identifer, $type);
        }
    }

    public function changeInvoiceContact($invoiceId, $toContactName)
    {
        if ($this->provider) {
            $this->provider->changeInvoiceContact($invoiceId, $toContactName);
        }
    }

    public function getNextInvoicePaymentAttemptsForUser($userId)
    {
        if (!isset($this->nextAttempts[$userId])) {
            $this->nextAttempts[$userId] = [];
            $invoiceEntity = new invoiceEntity();
            $nextAttempts = $invoiceEntity->getChargeable(10, $userId);

            $paymentEntity = new paymentEntity();
            $paymentEntity->getMostRecentForUser($userId);
            $lastPaymentForUser = \DateTime::createFromFormat('U', $paymentEntity->created); // Get the last payment attempt for this user
            if (!$lastPaymentForUser) {
                $lastPaymentForUser = (new \DateTime())->sub('P1D');
            }
            $nextPaymentForUser = clone $lastPaymentForUser;
            foreach ($nextAttempts as $invoice) {
                $nextPaymentForUser->add(new \DateInterval('P1D'));
                $now = new \DateTime('now');
                if ($nextPaymentForUser < $now) $nextPaymentForUser = $now;
                $this->nextAttempts[$userId][$invoice->id] = $nextPaymentForUser->format('d M Y') .' at '.$nextPaymentForUser->format('H:i');
            }
        }

        return $this->nextAttempts[$userId];
    }

    public function backfillUserInvoices($userId)
    {
        $currentUser = currentUser::getCurrentUser();

        $userEntity = new userEntity();
        $userEntity->load($userId);
        if ($userEntity->id ?? null) {

            $existingInvoices = [];
            $invoiceEntity = new invoiceEntity();
            $invoices = $invoiceEntity->getForUser($userId);
            foreach ($invoices as $inv) {
                $existingInvoices[trim($inv->invoice_number)] = 1;
            }

            $remoteInvoices = $this->getProviderInvoices($userEntity->email, true);

            $upserts = [];

            foreach ($remoteInvoices as $remoteInvoice) {

                if (!isset($existingInvoices[trim($remoteInvoice['name'])])) {

                    $upserts[] = [
                        'user_id' => $userId,
                        'invoice_date' => $remoteInvoice['dateMysql'],
                        'invoice_number' => trim($remoteInvoice['name']),
                        'total' => str_replace(",","",$remoteInvoice['subtotal']),
                        'vat' => str_replace(",","",$remoteInvoice['totalTax']),
                        'line_items' => '',
                        'payment_provider' => 'none',
                        'provider_id' => $remoteInvoice['id'],
                        'billing_enabled' => 0,
                        'send_receipt' => 0,
                        'created' => time(),
                        'created_by' => $currentUser->id,
                        'modified' => 0,
                        'modified_by' => 0,
                        'paid' => $remoteInvoice['paid'],
                        'credit_applied' => '0.00',
                        'provider' => $this->config->app->invoicing->provider ?? null
                    ];
                }
            }

            if (count($upserts) > 0) {
                Database::persistRawBatch($upserts, 'invoice');
            }
        }
    }
}


