<?php
/**
 * Copyright (C) Baluart.COM - All Rights Reserved
 *
 * @since 1.0
 * @author Baluart E.I.R.L.
 * @copyright Copyright (c) 2015 - 2021 Baluart E.I.R.L.
 * @license http://codecanyon.net/licenses/faq Envato marketplace licenses
 * @link https://easyforms.dev/ Easy Forms
 */
namespace app\modules\subscription\controllers;

use app\modules\subscription\models\Subscription;
use app\modules\subscription\models\SubscriptionPrice;
use app\modules\subscription\models\SubscriptionTransaction;
use app\modules\subscription\services\PaypalClient;
use app\modules\subscription\services\StripeClient;
use Exception;
use Yii;
use yii\filters\VerbFilter;
use yii\helpers\Inflector;
use yii\helpers\Json;
use yii\web\Controller;
use yii\web\HeaderCollection;
use yii\web\NotFoundHttpException;
use yii\web\Response;

/**
 * Class WebhookController
 * @package app\modules\subscription\controllers
 */
class WebhookController extends Controller
{
    /**
     * @var bool whether to enable CSRF validation for the actions in this controller
     */
    public $enableCsrfValidation = false;

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            'verbs' => [
                'class' => VerbFilter::class,
                'actions' => [
                    'handle-paypal-webhook' => ['post'],
                    'handle-stripe-webhook' => ['post'],
                ],
            ],
        ];
    }

    /**
     * Endpoint for Paypal webhooks
     *
     * @return mixed|Response
     */
    public function actionHandlePaypalWebhook()
    {
        $payload = json_decode(Yii::$app->request->getRawBody(), true);
        try {
            // Verify if webhook was sent by paypal
//        $headers = Yii::$app->request->getHeaders();
//        if (!$this->eventExistsOnPaypal($headers, $payload)) {
//            return new Response([
//                'statusCode' => 400,
//            ]);
//        }
            if (isset($payload['event_type'])) {
                if (in_array($payload['event_type'], ["PAYMENT.SALE.PENDING", "PAYMENT.SALE.COMPLETED"])) {
                    return $this->handlePaypalPayment($payload);
                } elseif ($payload['event_type'] === "BILLING.SUBSCRIPTION.CANCELLED") {
                    return $this->handlePaypalSubscriptionCancelled($payload);
                }
            }
        } catch (Exception $e) {
            Yii::error($e);
        }

        return $this->missingMethod();
    }

    /**
     * Endpoint for Stripe webhooks
     *
     * @return mixed|Response
     */
    public function actionHandleStripeWebhook()
    {
        $payload = json_decode(Yii::$app->request->getRawBody(), true);

        if (!$this->eventExistsOnStripe($payload['id'])) {
            return $this->missingMethod();
        }

        $method = 'handleStripe' . Inflector::camelize(str_replace('.', '_', $payload['type']));

        if (method_exists($this, $method)) {
            return $this->{$method}($payload);
        }

        return $this->missingMethod();
    }

    /**
     * Verify with Paypal that the event is genuine.
     *
     * @param HeaderCollection $headers
     * @param array $payload
     *
     * @return bool
     */
    protected function eventExistsOnPaypal($headers, $payload)
    {
        try {

            $paypalClient = new PaypalClient();
            return $paypalClient->verifyWebhookSignature($headers, $payload);

        } catch (Exception $e) {

            Yii::error($e);
            return false;

        }
    }

    /**
     * Verify with Stripe that the event is genuine.
     *
     * @param string $id
     *
     * @return bool
     */
    protected function eventExistsOnStripe($id)
    {
        try {

            $stripeClient = new StripeClient();
            return !is_null($stripeClient->retrieveEventByID($id));

        } catch (Exception $e) {

            Yii::error($e);
            return false;

        }
    }

    /**
     * Response when a webhook is handled
     *
     * @return Response
     */
    public function handledMethod()
    {
        return new Response([
            'statusCode' => 200,
            'statusText' => 'Webhook Handled',
        ]);
    }

    /**
     * Handle calls to missing methods on the controller.
     *
     * @return mixed
     */
    public function missingMethod()
    {
        return new Response([
            'statusCode' => 200,
        ]);
    }

    /**
     * Handle Webhook of Paypal Subscription Cancelled
     *
     * @param $payload
     * @throws NotFoundHttpException
     */
    public function handlePaypalSubscriptionCancelled($payload)
    {
        if (isset($payload['resource'], $payload['resource']['id'], $payload['resource']['status'])) {

            $model = $this->findModel(Subscription::GATEWAY_PAYPAL, $payload['resource']['id']);

            if ($model->type === SubscriptionPrice::TYPE_RECURRING
                && $model->gateway_status !== $payload['resource']['status']) {
                $endsAt = null;
                if (isset($payload['resource']['billing_info']['next_billing_time'])) {
                    $endsAt = strtotime($payload['resource']['billing_info']['next_billing_time']);
                } else if ($nextBillingTime = $model->displayInfo('next_billing_time')) {
                    $endsAt = $nextBillingTime;
                }
                $model->markAsCancelled($endsAt, $payload['resource']['status']);
            }
        }

        return $this->handledMethod();
    }

    /**
     * Handle Webhook of Paypal Payment
     *
     * @param $payload
     * @throws NotFoundHttpException
     */
    public function handlePaypalPayment($payload)
    {
        if (isset(
            $payload['resource'],
            $payload['resource']['id'],
            $payload['resource']['state'],
            $payload['resource']['billing_agreement_id']
        )) {

            $resource = $payload['resource'];

            $model = $this->findModel(Subscription::GATEWAY_PAYPAL, $resource['billing_agreement_id']);

            if ($model->isRecurring()) {

                // Validate Subscription via Paypal API
                $paypalClient = new PaypalClient();
                $paypalSubscription = $paypalClient->getSubscription($model->gateway_id);

                // Validate data
                if (isset($paypalSubscription->id)) {

                    $exists = SubscriptionTransaction::findOne(['gateway_transaction_id' => $resource['id']]);

                    if ($exists === null) {
                        // Save Transaction
                        $transaction = new SubscriptionTransaction();
                        $transaction->subscription_id = $model->id;
                        $transaction->gateway = Subscription::GATEWAY_PAYPAL;
                        $transaction->gateway_id = $resource['billing_agreement_id'];
                        $transaction->gateway_status = $resource['state'];
                        $transaction->gateway_transaction_id = $resource['id'];
                        $transaction->gateway_time = isset($resource['create_time']) ? strtotime($resource['create_time']) : null;;
                        $transaction->total = isset($resource['amount']['total']) ? $resource['amount']['total'] * 100 : null; // In cents
                        $transaction->currency_code = isset($resource['amount']['currency']) ? $resource['amount']['currency'] : null;
                        $transaction->payload = Json::encode($payload);
                        $transaction->created_by = $model->created_by;
                        $transaction->save();
                    }

                    // Is there a next subscription?
                    /** @var array $nextSubscription Is there a next subscription? **/
                    $nextSubscription = $model->getNextSubscription();
                    if (isset($nextSubscription['info']['next_billing_time'])) {
                        // Calculate next billing time
                        $nextBillingTime = isset($paypalSubscription->billing_info->next_billing_time)
                            ? strtotime($paypalSubscription->billing_info->next_billing_time)
                            : '';

                        // Update Subscription model
                        $model->product_id = $nextSubscription['product_id'];
                        $model->price_id = $nextSubscription['price_id'];
                        $model->name = $nextSubscription['name'];
                        $model->type = $nextSubscription['type'];
                        $model->trial_ends_at = null; // NULL because we are starting in a new billing period
                        $model->ends_at = null; // NULL because it's a recurring payment
                        $model->gateway_id = $nextSubscription['gateway_id'];
                        $model->gateway_status = $nextSubscription['gateway_status'];
                        $model->form_limit = $nextSubscription['form_limit'];
                        $model->submission_limit = $nextSubscription['submission_limit'];
                        $model->info = [ // Removes 'next_subscription' key
                            'currency_code' => $nextSubscription['info']['currency_code'],
                            'amount' => $nextSubscription['info']['amount'],
                            'interval' => $nextSubscription['info']['interval'],
                            'interval_count' => $nextSubscription['info']['interval_count'],
                            'description' => $nextSubscription['info']['description'],
                            'details' => $nextSubscription['info']['details'],
                            'next_billing_time' => $nextBillingTime,
                        ];
                        $model->save();

                        // Update User Roles
                        $model->updateUserRoles();

                    }
                }
            }
        }

        return $this->handledMethod();
    }

    /**
     * Handle Webhook of Stripe Customer Subscription Deleted
     *
     * @param array $payload
     * @return Response
     * @throws NotFoundHttpException
     */
    protected function handleStripeCustomerSubscriptionDeleted(array $payload)
    {
        if (isset($payload['data']['object']['id'], $payload['data']['object']['status'])) {

            $model = $this->findModel(Subscription::GATEWAY_STRIPE, $payload['data']['object']['id']);

            if ($model->isRecurring()) {
                $endsAt = null;
                if (isset($payload['data']['object']['current_period_end'])) {
                    $endsAt = $payload['data']['object']['current_period_end'];
                } else if ($nextBillingTime = $model->displayInfo('next_billing_time')) {
                    $endsAt = $nextBillingTime;
                }
                $model->markAsCancelled($endsAt, $payload['data']['object']['status']);
            }
        }

        return $this->handledMethod();
    }

    /**
     * Handle Webhook of Stripe Invoice Paid
     *
     * @param array $payload
     * @return Response
     * @throws NotFoundHttpException
     */
    protected function handleStripeInvoicePaid(array $payload)
    {
        if (isset($payload['data']['object']['id']
            , $payload['data']['object']['status']
            , $payload['data']['object']['subscription'])) {

            $invoice = $payload['data']['object'];

            $model = $this->findModel(Subscription::GATEWAY_STRIPE, $invoice['subscription']);

            if ($model->isRecurring()) {

                $exists = SubscriptionTransaction::findOne(['gateway_transaction_id' => $invoice['id']]);

                if ($exists === null) {
                    // Save Transaction
                    $transaction = new SubscriptionTransaction();
                    $transaction->subscription_id = $model->id;
                    $transaction->gateway = Subscription::GATEWAY_STRIPE;
                    $transaction->gateway_id = $invoice['subscription'];
                    $transaction->gateway_status = $invoice['status'];
                    $transaction->gateway_transaction_id = $invoice['id'];
                    $transaction->gateway_time = isset($invoice['created']) ? $invoice['created'] : null;
                    $transaction->total = isset($invoice['amount_paid']) ? $invoice['amount_paid'] : null;
                    $transaction->currency_code = isset($invoice['currency']) ? $invoice['currency'] : null;
                    $transaction->payload = Json::encode($payload);
                    $transaction->created_by = $model->created_by;
                    $transaction->save();
                }
            }
        }

        return $this->handledMethod();
    }

    /**
     * Finds the Subscription model based on its primary key value.
     * If the model is not found, a 404 HTTP exception will be thrown.
     * @param string $gateway
     * @param string $gateway_id
     * @return Subscription the loaded model
     * @throws NotFoundHttpException if the model cannot be found
     */
    protected function findModel($gateway, $gateway_id)
    {
        if (($model = Subscription::findOne([
                'gateway' => $gateway,
                'gateway_id' => $gateway_id,
            ])) !== null) {
            return $model;
        }

        throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist.'));
    }
}