<?php

namespace apexl\Io\modules\payment\services;

use apexl\Config\Singleton;
use apexl\encryption\Encrypt;
use apexl\Io\includes\Hook;
use apexl\Io\includes\Routes;
use apexl\Io\modules\company\entities\companyEntity;
use apexl\Io\modules\email\services\emailService;
use apexl\Io\modules\email\services\templateService;
use apexl\Io\modules\payment\entities\paymentEntity;
use apexl\Io\modules\payment\entities\paymentProviderRefEntity;
use apexl\Io\modules\payment\entities\paymentTokenEntity;
use apexl\Io\modules\payment\providers\paypalProvider;
use apexl\Io\modules\payment\providers\stripeProvider;
use apexl\Io\modules\product\entities\productEntity;
use apexl\Io\modules\subscription\entities\subscriptionEntity;
use apexl\Io\modules\subscription\services\subscriptionService;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\services\simService;
use apexl\Io\modules\user\services\userTools;
use apexl\Io\services\Database;
use apexl\Io\services\Logger;
use apexl\Io\services\Output;
use apexl\Io\services\pathUtility;
use Psr\Http\Message\ServerRequestInterface as Request;

class cardValidationService {

    protected $config;
    protected $provider;
    protected $providerName;
    protected $output;
    protected $path;
    protected $subscriptionId;
    protected $maxPaymentFailures = 3;  // If payment fails this many times, card will be abandoned and user will need to re-enter details
    protected $subscriptions = [];
    protected $chargeId = null;
    protected $paymentService;

    public function __construct()
    {
        $this->config = Singleton::getInstance();
        $this->provider = new stripeProvider();
        $this->providerName = 'stripe';
        $this->output = Output::getInstance();
        $this->path = pathUtility::getInstance();
        $this->paymentService = new paymentService();
    }

    protected function getProvider($providerName)
    {
        switch ($providerName) {
            case 'stripe':
                return (new stripeProvider());
            case 'paypal':
                return (new paypalProvider());
        }
    }

    public function createNewTokenEntity($userId, $data)
    {
        $paymentTokenEntity = new paymentTokenEntity();
        $paymentTokenEntity->user_id = $userId;
        $paymentTokenEntity->created = time();
        $paymentTokenEntity->provider = $this->getProviderName();
        $paymentTokenEntity->data = json_encode($data);
        $paymentTokenEntity->store();

        return $paymentTokenEntity;
    }

    public function getOneTimePaymentUrl($products, $successUrl, $cancelUrl)
    {
        if (!$successUrl) $successUrl = $this->config->app->site->backend_domain . Routes::getRoutePattern('payment.validation.success');
        if (!$cancelUrl) $cancelUrl = $this->config->app->site->backend_domain . Routes::getRoutePattern('payment.validation.cancel');

        return $this->provider->getOneTimePaymentUrl($products, $successUrl, $cancelUrl);
    }

    public function getPaymentIntentIdFromSessionId($sessionId)
    {
        return $this->provider->getPaymentIntentIdFromSessionId($sessionId);
    }

    public function getProviderPortalUrl($paymentToken, $successUrl=null, $cancelUrl=null, $meta=null)
    {
        if (!$successUrl) $successUrl = $this->config->app->site->backend_domain . Routes::getRoutePattern('payment.validation.success');
        if (!$cancelUrl) $cancelUrl = $this->config->app->site->backend_domain . Routes::getRoutePattern('payment.validation.cancel');
        $providerCustomerRef = $paymentToken->user_id ? $this->getProviderUserRef($paymentToken->user_id) : null;

        return $this->provider->getCardValidationUrl($successUrl, $cancelUrl, $paymentToken->id, $providerCustomerRef, $meta);
    }

    public function getProviderTokenUsingSessionId($sessionId)
    {
        return $this->provider->retrieveToken($sessionId);
    }

    public function completeValidation(Request $request, $paymentToken, $userEmail, $userFullName, $password=null, $providerSessionId=null, $meta=null)
    {
        $user = new userEntity();
        $user->getUserByEmail($userEmail);
        if (!$user->id){
            // Create account
            $userTools = new userTools();
            list($firstName, $lastName) = $userTools->splitNameToFirstAndLast($userFullName);
            $user = $userTools->createCompanyUserWithPassword($request, $userEmail, $firstName, $lastName, $password, "$firstName $lastName", true, $meta);
        }

        if ($user) {
            $encrypt = new Encrypt();
            $paymentMethodRefDecrypted = $encrypt->decrypt($paymentToken->payment_method_ref, $paymentToken->payment_method_ref_iv, $this->config->app->encryption->key);
            if ($customerId = $this->attachCustomerToPaymentMethod($paymentMethodRefDecrypted, $user)) {
                //$paymentToken->company_id = $company->id;
                $paymentToken->user_id = $user->id;
                $paymentToken->setup_complete = 1; // Mark as setup complete
                $paymentToken->store();

                $this->output::addMessage($this->path->getRouteName($request), 'success','Payment setup complete');
            } else {
                die('Failed to create customr record in payment provider');
            }
        }

        return $user;
    }

    public function linkSubscriptionsToPaymentMethod($createdSubscriptions, $paymentToken, $providerSessionId){

        if (count($createdSubscriptions)) { // Add subscription ID meta to payment method
            $config = Singleton::getInstance();
            $encrypt = new Encrypt();
            $paymentMethodRefDecrypted = $encrypt->decrypt($paymentToken->payment_method_ref, $paymentToken->payment_method_ref_iv, $config->app->encryption->key);
            $meta = array_merge($this->provider->getMetaDataFromSession($providerSessionId, ['subscription_ids' => implode(",",array_keys($createdSubscriptions))]));
            $this->provider->updatePaymentMethod($paymentMethodRefDecrypted, $meta);
        }
    }

    public function getSubscriptions()
    {
        return $this->subscriptions;
    }

    public function getChargeId()
    {
        return $this->chargeId;
    }

    /*public function getSubscriptionId()
    {
        return $this->subscriptionId;
    }

    public function getSubscription()
    {
        return $this->subscription;
    }*/

    public function getProviderName()
    {
        return $this->providerName;
    }


    public function capturePaymentWithToken($userId, $providerName=null, $amount, $ref="", $isSubscriptionPayment=true, $data=null, $meta=null, $transactionData = null, $userPresent=false, $creditApplied=0)
    {
        $userEntity = new userEntity();
        $userEntity->load($userId);

        $this->chargeId = null;
        $paymentTokenEntity = new paymentTokenEntity();

        $latestToken = $paymentTokenEntity->getBillingTokenForUser($userId, $providerName); // Get last payment token for user

        // Check whether token should now be abandoned
        if (isset($latestToken['id']) && !$paymentTokenEntity->checkDisableToken($latestToken, $this->maxPaymentFailures)){
            return null;
        }
        $description = '';

            $encrypt = new Encrypt();
            $paymentMethodRefDecrypted = $encrypt->decrypt($latestToken['payment_method_ref'], $latestToken['payment_method_ref_iv'], $this->config->app->encryption->key);
            if ($paymentMethodRefDecrypted) {
                $paymentProviderRefEntity = new paymentProviderRefEntity();
                $customerRef = $paymentProviderRefEntity->getUserRef($this->providerName, $userId);
                if (!is_null($data)) {
                    $description = json_encode($data);
                }
                if ($isSubscriptionPayment) {
                    if (!isset($meta['subscription_ids'])) $meta['subscription_ids'] = [];
                    foreach ($this->subscriptions as $subscription) {
                        $meta['subscription_ids'][] = $subscription->id;
                    }
                    $meta['subscription_ids'] = implode(",", $meta['subscription_ids']);
                }
                if ($creditApplied > 0) {
                    $meta['credit_applied'] = $creditApplied;
                    $meta['email'] = $userEntity->email;
                }

                $payment = new paymentEntity();
                $payment->provider = $providerName;
                $payment->provider_ref = "none";
                $payment->status = 'unsubmitted';
                $payment->amount = $amount;
                $payment->payment_token_id = $latestToken['id'];
                $payment->user_id = $userId;
                $payment->ref = $ref;
                $payment->store();
                $paymentId = $payment->id;

                list($piId, $chargeId, $threeDSecureUrl, $errorMsg) = $this->getProvider($providerName)->capturePaymentWithToken($paymentMethodRefDecrypted, ($customerRef['ref'] ?? null), $amount, $description, 'gbp', $data, $meta, $transactionData, $this->config->app->site->frontend_domain. Routes::getRoutePattern('payment.display.threedsecure-complete', ['publicId' => $payment->public_id]));
                $this->chargeId = $chargeId;
                //$piId = false;
                $payment = new paymentEntity();
                $payment->load($paymentId, true);
                $payment->provider_ref = $piId;
                $payment->provider_ref_2 = $chargeId;
                $payment->payment_token_id = $latestToken['id'];
                $payment->status = $chargeId ? 'successful' : 'failed';
                if ($threeDSecureUrl) {
                    $payment->status = 'three_d_secure_required';
                    $payment->threedsecure_url = $threeDSecureUrl;
                }
                $payment->amount = $amount;
                if (!is_null($data)) {
                    $payment->data = json_encode($data);
                    if (isset($data->order_id)) $payment->remote_order_id = $data->order_id;
                }

                $payment->store();

                if ($isSubscriptionPayment) $this->createSubscriptionPaymentLinks($payment);

            $this->processPaymentResult($payment, $userPresent, $errorMsg);

                return $payment;
        } else {
            $paymentTokenEntity->abandonToken($tokenId, "Token decryption failure"); // Cannot decrypt token
            }

        return null;
    }

    public function processPaymentResult($paymentEntity, $userPresent=false, $errorMsg=null)
    {
        $paymentTokenEntity = new paymentTokenEntity();
        $paymentTokenEntity->load($paymentEntity->payment_token_id);
        $subscriptionEntity = new subscriptionEntity();
        $subscriptionEntity->getByPaymentId($paymentEntity->id); // @TODO one payment can cover multiple subscriptions
        $userEntity = new userEntity();
        $userEntity->load($paymentEntity->user_id);

        if (isset($subscriptionEntity->id) && $subscriptionEntity->id) {
            $this->subscriptions[$subscriptionEntity->id] = $subscriptionEntity;
        }


            switch ($paymentEntity->status) {
                case 'successful':
                    // Temporary if statement to get quick results for ED - code shouldn't really be here
                    $data = json_decode($paymentTokenEntity->data);

                    $paymentTokenEntity->num_failures = 0; // reset number of failures
                    $paymentTokenEntity->store();

                    // Disable all other active 3D Secure links for this subscription
                if (isset($subscriptionEntity->id) && $subscriptionEntity->id) {
                    $paymentService = new paymentService();
                    $paymentService->disableActiveThreeDSecureLinks($subscriptionEntity->id);
                }

                    if (!is_null($data) && isset($paymentEntity->remote_order_id) && $paymentEntity->remote_order_id) {
                        $woocommerceService = new woocommerceIntegrationService();
                        $woocommerceService->notifyOrderSuccess($paymentEntity->remote_order_id, $paymentEntity->chargeId);
                    }
                    break;
                case 'three_d_secure_required':
                    // Send automated email to the customer with link to complete 3D Secure
                    if (!$userPresent) {
                    $this->paymentThreeDSecureNotification($userEntity, $paymentEntity);
                    }
                    break;
                case 'failed':
                    $paymentTokenEntity->num_failures++;
                    $paymentTokenEntity->store();
                    if ($paymentTokenEntity->num_failures >= $this->maxPaymentFailures) {
                    $paymentTokenEntity->abandonToken($paymentTokenEntity->id, "Max payment failures reached ({$this->maxPaymentFailures}). Ref1");
                    }
                Hook::processHook('payment_failed', $paymentEntity, $errorMsg);
                    break;
            }

    }


    public function setSubscriptions(array $subscriptions)
    {
        $entities = [];
        foreach ($subscriptions as $subscription) {
            if ($subscription instanceof subscriptionEntity) {
                $entities[] = $subscription;
            } else {
                $subscriptionEntity = new subscriptionEntity();
                $subscriptionEntity->load($subscription->id);
                $entities[] = $subscriptionEntity;
            }
        }
        $this->subscriptions = $entities;
    }

    public function createSubscriptionPaymentLinks($payment)
    {

        $upserts = [];
        foreach ($this->subscriptions as $subscription) {
            $upserts[] = [
                'subscription_id' => $subscription->id,
                'payment_id' => $payment->id,
            ];
        }
        if (count($upserts) > 0) {
            Database::persistRawBatch($upserts, 'subscription_payment');
        }
    }

    protected function paymentThreeDSecureNotification(userEntity $userEntity, paymentEntity $payment)
    {

        $frontEndDomain = $this->config->app->site->frontend_domain ?? 'localhost';
        $link = rtrim($frontEndDomain, '/').'/payment/threedsecure/'.$payment->public_id;

        if ($userEntity->id) {

            $emailData = [
                'payment_amount' => number_format($payment->amount,2),
                'payment_authorization_link' => $link,
                'name' => $userEntity->first_name,
            ];
            $subscriptionIds = [];
            foreach ($this->subscriptions as $subscription) {
                $subscriptionIds[] = $subscription->id;
            }

            $customData = Hook::processHook('pre_send_3dsecure_authorization_email', $subscriptionIds);
            if (is_array($customData)) $emailData = array_merge($customData, $emailData);

            $emailService = new emailService();
            $result = $emailService->send(
                (object) [
                    'to' => $userEntity->email,
                    'subject' => 'Payment Authorisation Required',
                    'body' => templateService::fetch("3dsecure_required", $emailData),
                    'user_id' => $userEntity->id,
                ],
                '3dsecure_required'
            );

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

    protected function getProductIdFromToken($paymentToken)
    {
        $data = json_decode($paymentToken->data);
        return $data->productId ?? null;
    }

    public function attachCustomerToPaymentMethod($token, $user, $forceAttachInStripe=FALSE)
    {
        if ($ref = $this->getProviderUserRef($user->id) && !$forceAttachInStripe) return $ref;

        $customerId = $this->provider->attachCustomerToPaymentMethod($token, $user->email, $user->getNiceName, $ref);
        if ($customerId && !$ref) {
            $paymentProviderRef = new paymentProviderRefEntity();
            $paymentProviderRef->user_id = $user->id;
            $paymentProviderRef->provider = $this->providerName;
            $paymentProviderRef->ref = $customerId;
            $paymentProviderRef->store();
        }

        return $customerId;
    }

    protected function getProviderUserRef($userId)
    {
        $paymentProviderRef = new paymentProviderRefEntity();
        $paymentProviderRef->getUserRef($this->providerName, $userId);
        return $paymentProviderRef->ref ?? null;
    }
}