<?php

namespace apexl\Io\modules\payment\providers;

use apexl\Config\Singleton;
use apexl\Io\modules\payment\entities\paymentLogEntity;
use Stripe\Checkout\Session;
use Stripe\Stripe;
use Stripe\StripeClient;
use Stripe\Event;
use Stripe\Webhook;

class stripeProvider implements providerInterface {

    protected $config;
    protected $stripe;
    protected $testMode;

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

        if ($this->testMode = ($this->config->app->payments->testMode ?? false) && isset($this->config->app->payments->stripe->secret_key_test)) {
            Stripe::setApiKey($this->config->app->payments->stripe->secret_key_test);
            $this->stripe = new StripeClient($this->config->app->payments->stripe->secret_key_test);
        } else {
            Stripe::setApiKey($this->config->app->payments->stripe->secret_key);
            $this->stripe = new StripeClient($this->config->app->payments->stripe->secret_key);
        }

    }

    public function getPaymentMethodsForCustomer($providerCustomerRef)
    {
        return $this->stripe->customers->allPaymentMethods(
            $providerCustomerRef,
            ['type' => 'card']
        );
    }

    public function updateCustomer($custRef, $updates)
    {
        $this->makeRequest('customers', 'update', $custRef, $updates);
    }

    public function processWebhookRequest($payload)
    {
        $event = null;
        $error = null;
        $providerRef = null;

        try {
            $event = Event::constructFrom(
                json_decode($payload, true)
            );
        } catch(\UnexpectedValueException $e) {
            // Invalid payload
            return [null, 0, null, 'Webhook error while parsing basic request.', null, null, null, null, null];
        }

        $webhookSecret = $this->testMode && isset($this->config->app->payments->stripe->webhook_secret_test) ? $this->config->app->payments->stripe->webhook_secret_test : $this->config->app->payments->stripe->webhook_secret;

        if ($webhookSecret) {
            // Only verify the event if there is an endpoint secret defined
            // Otherwise use the basic decoded event

            if (!isset($_SERVER['HTTP_STRIPE_SIGNATURE'])) return [null, 0, null, 'Missing stripe signature header.', null, null, null, null, null];

            $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
            try {
                $event = Webhook::constructEvent(
                    $payload, $sig_header, $webhookSecret
                );
            } catch(\Stripe\Exception\SignatureVerificationException $e) {
                // Invalid signature
                return [null, 0, null, 'Webhook error while validating signature.', null, null, null, null, null];
            }
        }

        $object = $event->data->object ?? null;
        $providerRef = $object->id ?? null;

        // Handle the event
        switch ($event->type) {
            case 'payment_intent.succeeded':
                $paymentIntent = $object;
                return ["payment_succeeded", 1, $providerRef, $error, $paymentIntent->metadata ?? null, null, $this->isFraudulent($paymentIntent), $this->getRiskLevel($paymentIntent), $this->getChargeId($paymentIntent)];
                break;
            case 'payment_intent.payment_failed':
                $paymentIntent = $object;
                return ["payment_failed", 1, $providerRef, $error, $paymentIntent->metadata ?? null, $this->paymentError($paymentIntent), $this->isFraudulent($paymentIntent), $this->getRiskLevel($paymentIntent), null];
                break;
        }

        return ["not_setup", 1, $providerRef, $error, null, null, null, null, null];
    }

    protected function paymentError($paymentIntent)
    {
        return $paymentIntent->last_payment_error->message ?? null;
        }

    protected function isFraudulent($paymentIntent)
    {
        $check1 = $paymentIntent->last_payment_error->decline_code ?? null;
        $check2 = $paymentIntent->charges->data[0]->fraud_details->stripe_report ?? null;

        return ($check1 == 'fraudulent' || $check2 == 'fraudulent');
    }

    protected function getRiskLevel($paymentIntent)
    {
        return $paymentIntent->charges->data[0]->outcome->risk_level ?? null;
    }

    protected function getChargeId($paymentIntent)
    {

        $charge = $paymentIntent->latest_charge ?? null;
        if (!$charge) $charge = $paymentIntent->charges->data[0]->id ?? null;

        return $charge;
    }

    public function getOneTimePaymentUrl($products, $successUrl, $cancelUrl)
    {
        $options = [
            'line_items' => [],
            'mode' => 'payment',
            'success_url' => $successUrl.'/{CHECKOUT_SESSION_ID}',
            'cancel_url' => $cancelUrl.'/{CHECKOUT_SESSION_ID}',
        ];

        foreach ($products as $product) {
            if (is_object($product)) {
                $product = (array)$product;
            }
            $options['line_items'][] = [
                'price_data' => [
                    'currency' => 'gbp',
                    'product_data' => [
                        'name' => $product['name'],
                    ],
                    'unit_amount' => ($product['unit_price']*100), // stripe amounts are in pence
                ],
                'quantity' => $product['quantity'],
            ];
        }

        $session = Session::create($options);

        return $session->url;
    }
    
    public function getCardValidationUrl($successUrl, $cancelUrl, $localTokenId, $customerRef, $meta)
    {
        $options = [
            'payment_method_types' => ['card'],
            'mode' => 'setup',
            'success_url' => "$successUrl/{CHECKOUT_SESSION_ID}/$localTokenId",
            'cancel_url' => $cancelUrl,
        ];
        if ($customerRef) $options['customer'] = $customerRef;
        if ($meta) $options['metadata'] = $meta;

        $session = Session::create($options);

        $paymentLog = new paymentLogEntity();
        $paymentLog->ts = date('Y-m-d H:i:s');
        $paymentLog->provider = 'stripe';
        $paymentLog->api_name = 'checkout';
        $paymentLog->method_name = 'sessions > create';
        if (isset($session->id)) $paymentLog->provider_id = $session->id;
        $paymentLog->request_data = json_encode($options);
        $paymentLog->response_data = json_encode($session);
        $paymentLog->store();

        if ($meta && isset($session->setup_intent) && $session->setup_intent) { // Add metadata to setup_intent
            $this->setMetaOnSetupintent($session->setup_intent, $meta);
        }

        return $session->url;
    }

    public function getMetaDataFromSession($sessionId)
    {
        $session = $this->makeRequest('checkout', 'sessions', $sessionId, [], "retrieve");

        return $session->metadata->toArray();
    }

    public function getSetupIntentIdFromSessionId($sessionId)
    {
        $session = $this->makeRequest('checkout', 'sessions', $sessionId, [], "retrieve");

        return $session->setup_intent ?? null;
    }

    public function getPaymentIntentIdFromSessionId($sessionId)
    {
        $session = $this->makeRequest('checkout', 'sessions', $sessionId, [], "retrieve");

        return $session->payment_intent ?? null;
    }

    public function retrieveToken($sessionId)
    {
        $session = $this->makeRequest('checkout', 'sessions', $sessionId, [], "retrieve");
        //$session = $this->stripe->checkout->sessions->retrieve($sessionId, []);
        $paymentMethodId = $this->retrievePaymentMethodFromIntent($session->setup_intent);
        $this->setupOffSessionUsage($session->setup_intent);

        return [$session->setup_intent ?? null, $paymentMethodId];
    }

    public function updatePaymentMethod($paymentMethodId, $meta)
    {
        $this->makeRequest('paymentMethods', 'update', $paymentMethodId, ['metadata' => $meta]);
    }

    public function attachCustomerToPaymentMethod($paymentMethodId, $email, $fullName, $customerId=null)
    {
        if (!$customerId) $customerId = $this->createCustomer($email, $fullName);
        if ($customerId) {
            $response = $this->makeRequest('paymentMethods', 'attach', $paymentMethodId, ['customer' => $customerId]);
            /*$response = $this->stripe->paymentMethods->attach(
                $paymentMethodId,
                ['customer' => $customerId]
            );*/

            return $response->customer ?? false;
        }

        return false;
    }
    
    public function capturePaymentWithToken($paymentMethodId, $customerRef=null, $amount=0, $description = '', $currency='gbp', $data=null, $metaData=null, $transactionData, $threeDSecureReturnUrl=null)
    {
        $paymentIntentId = $this->createPaymentIntent($paymentMethodId, $customerRef, $amount, $description, $currency='gbp', $metaData);

        if ($paymentIntentId) {
            $params = [
                'payment_method' => $paymentMethodId,
            ];
            if ($threeDSecureReturnUrl) $params['return_url'] = $threeDSecureReturnUrl;
            $response = $this->makeRequest('paymentIntents', 'confirm', $paymentIntentId, $params);

            $chargeId = $response->charges->data[0]->id ?? null;
            if (!$chargeId) $chargeId = $response->latest_charge ?? null;
            $threeDSecureUrl = isset($response->status) && in_array($response->status, ['requires_action', 'requires_source_action']) && isset($response->next_action->redirect_to_url->url) ? $response->next_action->redirect_to_url->url : null;

            if (isset($response->status) && in_array($response->status, ['succeeded', 'requires_action', 'requires_source_action'])) return [$paymentIntentId, $chargeId, $threeDSecureUrl, null];
        }

        return [($paymentIntentId ? $paymentIntentId : false), false, false, $response->Error ?? null];
    }

    public function getPaymentMethod($paymentMethodId)
    {
        $paymentMethod = $this->makeRequest('paymentMethods', 'retrieve', $paymentMethodId);

        return isset($paymentMethod->id) ? $paymentMethod : null;
    }

    public function updateMetaInProvider($sessionId, $meta)
    {
        $setupIntentId = $this->getSetupIntentIdFromSessionId($sessionId);
        if ($setupIntentId) {
            $this->addMetaToSetupIntent($setupIntentId, $meta);
        }
    }

    protected function createPaymentIntent($paymentMethodId, $customerRef, $amount, $description = '', $currency='gbp', $metaData=null)
    {
        if ($amount > 0) {
            $options = [
                'amount' => round(($amount*100)), // Stripe deals in pence or cents etc.  i.e. the smallest currency unit
                'currency' => $currency,
                'payment_method' => $paymentMethodId,
                'setup_future_usage' => 'off_session',
                'description' => $description,
                'metadata' => (array)$metaData,
            ];
            if ($customerRef) $options['customer'] = $customerRef;

            $response = $this->makeRequest('paymentIntents', 'create', null, $options);
            //$response = $this->stripe->paymentIntents->create($options);
        }

        return $response->id ?? null;
    }

    protected function retrievePaymentMethodFromIntent($setupIntentId)
    {
        $setupIntent = $this->makeRequest('setupIntents', 'retrieve', $setupIntentId);
        //$setupIntent = $this->stripe->setupIntents->retrieve($setupIntentId, []);

        return $setupIntent->payment_method ?? null;
    }

    protected function setMetaOnSetupintent($setupIntentId, $meta)
    {
        $this->makeRequest('setupIntents', 'update', $setupIntentId, ['metadata' => $meta]);

        return;
    }

    public function addMetaToSetupIntent($setupIntentId, $meta)
    {
        // Get existing meta
        $setupIntent = $this->makeRequest('setupIntents', 'retrieve', $setupIntentId);
        $existingMeta = $setupIntent->metadata;

        // Merge with new meta and update
        $existingMeta = $existingMeta->toArray();
        $newMeta = array_merge($existingMeta, $meta);
        $this->setMetaOnSetupintent($setupIntentId, $newMeta);
    }

    protected function setupOffSessionUsage($setupIntentId)
    {
        $setupIntent = $this->makeRequest('setupIntents', 'retrieve', $setupIntentId);
        //$setupIntent = $this->stripe->setupIntents->retrieve($setupIntentId, []);
        if (isset($setupIntent->usage) && $setupIntent->usage != "off_session") {
            $this->makeRequest('setupIntents', 'update', $setupIntentId, ['usage' => 'off_session']);
            /*$this->stripe->setupIntents->update(
                $setupIntentId,
                ['usage' => 'off_session']
            );*/
        }
        return;
    }

    protected function createCustomer($email, $fullName)
    {
        $response = $this->makeRequest('customers', 'create', null, [
            'email' => $email,
            'name' => $fullName,
        ]);

        /*$response = $this->stripe->customers->create([
            'email' => $email,
            'name' => $fullName,
        ]);*/

        return $response->id ?? null;
    }

    protected function makeRequest($apiName, $methodName, $id=null, $data=[], $subMethodName=null)
    {
        $logData = $data;

        try {

            if (!is_null($id)) {
                if ($subMethodName) {
                    $response = $this->stripe->$apiName->$methodName->$subMethodName($id, $data);
                    $methodName = "$methodName > $subMethodName";
                } else {
                    $response = $this->stripe->$apiName->$methodName($id, $data);
                }
            } else {
                $response = $this->stripe->$apiName->$methodName($data);
            }

            $logResponse = $response;

        } catch(\Stripe\Exception\CardException $e) { // Payment decline
            $logResponse = (object)[
                'Error' => 'Stripe API: Payment Exception. ' . $e->getMessage(),
                'Status' => $e->getHttpStatus(),
                'Type' => $e->getError()->type,
                'Code' => $e->getError()->code,
                'Param' => $e->getError()->param,
                'Message' => $e->getError()->message
            ];
        } catch (\Stripe\Exception\RateLimitException $e) {
            $logResponse = (object)['Error' => 'Stripe API: rate limit exceeded. ' . $e->getMessage()];
        } catch (\Stripe\Exception\InvalidRequestException $e) {
            $logResponse = (object)['Error' => 'Stripe API: Invalid parameters were supplied. ' . $e->getMessage()];
        } catch (\Stripe\Exception\AuthenticationException $e) {
            $logResponse = (object)['Error' => 'Stripe API: Authentication failed. ' . $e->getMessage()];
        } catch (\Stripe\Exception\ApiConnectionException $e) {
            $logResponse = (object)['Error' => 'Stripe API: Network communication failed. ' . $e->getMessage()];
        } catch (\Stripe\Exception\ApiErrorException $e) {
            $logResponse = (object)['Error' => 'Stripe API: Generic API failure. ' . $e->getMessage()];
        } catch (\Exception $e) {
            $logResponse = (object)['Error' => $e->getMessage()];
        }

        $hideFields = [
            'payment_method'
        ];
        foreach ($hideFields as $field) {
            if (isset($logResponse->$field)) $logResponse->field = 'hidden_for_security';
            if (isset($logData[$field])) $logData[$field] = 'hidden_for_security';
        }

        $hideIdsForApis = [
            'paymentMethods'
        ];
        foreach ($hideIdsForApis as $method) {
            if ($apiName == $method) {
                $id = 'hidden_for_security';
                if (isset($logResponse->id)) $logResponse = ['hidden_for_security'];
                if (isset($logData['id'])) $logData['id'] = 'hidden_for_security';
            }
        }

        $paymentLog = new paymentLogEntity();
        $paymentLog->ts = date('Y-m-d H:i:s');
        $paymentLog->provider = 'stripe';
        $paymentLog->api_name = $apiName;
        $paymentLog->method_name = $methodName;
        if ($id) $paymentLog->provider_id = $id;
        elseif (isset($logResponse->id)) $paymentLog->provider_id = $logResponse->id;
        $paymentLog->request_data = json_encode($logData);
        $paymentLog->response_data = json_encode($logResponse);
        $paymentLog->store();

        return $response ?? $logResponse;
    }


}