<?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\models;

use app\components\behaviors\DateTrait;
use app\helpers\ArrayHelper;
use app\helpers\CurrencyHelper;
use app\models\User;
use app\modules\subscription\services\PaypalClient;
use app\modules\subscription\services\StripeClient;
use Carbon\Carbon;
use Yii;
use yii\behaviors\BlameableBehavior;
use yii\behaviors\TimestampBehavior;
use yii\helpers\Json;

/**
 * This is the model class for table "{{%subscription}}".
 *
 * @property int $id
 * @property int $user_id
 * @property int|null $product_id
 * @property int|null $price_id
 * @property string|null $name
 * @property string|null $type
 * @property int|null $trial_ends_at
 * @property int|null $ends_at
 * @property string|null $gateway
 * @property string|null $gateway_id
 * @property string|null $gateway_status
 * @property int $recommended
 * @property int $form_limit
 * @property int $submission_limit
 * @property int $file_upload_limit
 * @property int $file_storage_limit
 * @property int $api_request_limit
 * @property int $branding
 * @property string|null $info
 * @property int|null $downgraded_at
 * @property int $created_by
 * @property int|null $updated_by
 * @property int|null $created_at
 * @property int|null $updated_at
 *
 * @property User $user
 * @property SubscriptionProduct $product
 * @property SubscriptionPrice $price
 */
class Subscription extends \yii\db\ActiveRecord
{

    use DateTrait;

    const ON = 1;
    const OFF = 0;

    const GATEWAY_PAYPAL = 'paypal';
    const GATEWAY_STRIPE = 'stripe';

    /**
     * {@inheritdoc}
     */
    public static function tableName()
    {
        return '{{%subscription}}';
    }

    /**
     * @inheritdoc
     */
    public static function primaryKey()
    {
        return ['id'];
    }

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            BlameableBehavior::class,
            TimestampBehavior::class,
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            [['user_id', 'product_id', 'price_id', 'trial_ends_at', 'ends_at',
                'recommended', 'form_limit', 'submission_limit', 'file_upload_limit',
                'file_storage_limit', 'api_request_limit', 'branding', 'downgraded_at',
                'created_by', 'updated_by', 'created_at', 'updated_at'], 'integer'],
            [['name', 'gateway_id', 'gateway_status'], 'string', 'max' => 255],
            [['gateway'], 'string', 'max' => 20],
            [['type'], 'string', 'max' => 32],
            [['form_limit', 'submission_limit'], 'number', 'min' => 1, 'max' => 99999999999],
            [['info'], 'safe'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('app', 'ID'),
            'user_id' => Yii::t('app', 'User ID'),
            'product_id' => Yii::t('app', 'Product ID'),
            'price_id' => Yii::t('app', 'Price ID'),
            'name' => Yii::t('app', 'Name'),
            'type' => Yii::t('app', 'Type'),
            'gateway' => Yii::t('app', 'Gateway'),
            'gateway_id' => Yii::t('app', 'Gateway ID'),
            'gateway_status' => Yii::t('app', 'Gateway Status'),
            'recommended' => Yii::t('app', 'Recommended'),
            'form_limit' => Yii::t('app', 'Form Limit'),
            'submission_limit' => Yii::t('app', 'Submission Limit'),
            'file_upload_limit' => Yii::t('app', 'File Upload Limit'),
            'file_storage_limit' => Yii::t('app', 'File Storage Limit'),
            'api_request_limit' => Yii::t('app', 'Api Request Limit'),
            'branding' => Yii::t('app', 'Branding'),
            'trial_ends_at' => Yii::t('app', 'Trial Ends At'),
            'ends_at' => Yii::t('app', 'Ends At'),
            'info' => Yii::t('app', 'Info'),
            'downgraded_at' => Yii::t('app', 'Downgraded At'),
            'created_by' => Yii::t('app', 'Created By'),
            'updated_by' => Yii::t('app', 'Updated By'),
            'created_at' => Yii::t('app', 'Created At'),
            'updated_at' => Yii::t('app', 'Updated At'),
        ];
    }

    /**
     * @inheritdoc
     */
    public function afterFind()
    {
        // Decode json as assoc array
        $this->info = json_decode($this->info, true);

        parent::afterFind();
    }

    /**
     * @inheritdoc
     */
    public function beforeSave($insert)
    {

        if (!parent::beforeSave($insert)) {
            return false;
        }

        // Encode additional information
        if (!is_string($this->info)) {
            $this->info = Json::encode($this->info); // Encode array as json assoc array and UTF-8
        }

        return true;
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getUser()
    {
        return $this->hasOne(User::class,['id'=>'user_id']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getProduct()
    {
        return $this->hasOne(SubscriptionProduct::class,['id'=>'product_id']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getPrice()
    {
        return $this->hasOne(SubscriptionPrice::class,['id'=>'price_id']);
    }

    /**
     * Determine if the subscription is active, on trial, or within its grace period.
     *
     * @return bool
     */
    public function valid()
    {
        return $this->active() || $this->onTrial() || $this->onGracePeriod();
    }

    /**
     * Determine if the subscription is active.
     *
     * @return bool
     */
    public function active()
    {
        return is_null($this->ends_at) || $this->onGracePeriod();
    }

    /**
     * Determine if the subscription is no longer active.
     *
     * @return bool
     */
    public function cancelled()
    {
        return $this->isRecurring() && !is_null($this->ends_at);
    }

    /**
     * Determine if the subscription is within its trial period.
     *
     * @return bool
     */
    public function onTrial()
    {
        if (!is_null($this->trial_ends_at)) {
            $trialEndsAt = Carbon::createFromTimestampUTC($this->trial_ends_at);
            return Carbon::today()->lt($trialEndsAt);
        } else {
            return false;
        }
    }

    /**
     * Determine if the subscription is within its grace period after cancellation.
     *
     * @return bool
     */
    public function onGracePeriod()
    {
        if (!is_null($this->ends_at)) {
            $endsAt = Carbon::createFromTimestampUTC($this->ends_at);
            return Carbon::now()->lt(Carbon::instance($endsAt));
        } else {
            return false;
        }
    }

    /**
     * Cancel the subscription at the end of the billing period.
     *
     * @return $this
     * @throws \Stripe\Exception\ApiErrorException
     */
    public function cancel()
    {
        // For recurring payments, we need to cancel the subscription on payment gateways too
        if ($this->isRecurring()) {
            if ($this->gateway === self::GATEWAY_PAYPAL) {
                $this->cancelPaypalSubscription();
            } elseif ($this->gateway === self::GATEWAY_STRIPE) {
                $this->cancelStripeSubscription();
            }
        }

        // If the user was on trial, we will set the grace period to end when the trial
        // would have ended. Otherwise, we'll retrieve the end of the billing period
        // period and make that the end of the grace period for this current user.
        if ($this->onTrial()) {
            $this->ends_at = $this->trial_ends_at;
        }

        $this->save();

        return $this;
    }

    /**
     * Cancel the subscription immediately.
     *
     * @return $this
     * @throws \Stripe\Exception\ApiErrorException
     */
    public function cancelNow()
    {
        // For recurring payments, we need to cancel the subscription on payment gateways too
        if ($this->type === SubscriptionPrice::TYPE_RECURRING) {
            if ($this->gateway === self::GATEWAY_PAYPAL) {
                $this->cancelPaypalSubscription();
            } elseif ($this->gateway === self::GATEWAY_STRIPE) {
                $this->cancelStripeSubscription();
            }
        }

        $this->markAsCancelled();

        return $this;
    }

    /**
     * Mark the subscription as cancelled.
     *
     * @param int|null $endsAt
     * @param string $cancelled
     */
    public function markAsCancelled($endsAt = null, $cancelled = 'CANCELLED')
    {

        if (empty($endsAt)) {
            $endsAt = time();
        }

        $this->ends_at = $endsAt;
        if ($this->isRecurring()) {
            $this->gateway_status = $cancelled;
        }
        $this->save();
    }

    /**
     * Cancel Strip Subscription
     *
     * @throws \Stripe\Exception\ApiErrorException
     */
    public function cancelStripeSubscription()
    {
        $stripeClient = new StripeClient();
        $stripeSubscription = $stripeClient->getSubscription($this->gateway_id);
        if (isset($stripeSubscription->status)) {
            if ($stripeSubscription->status !== 'canceled') {
                if ($stripeSubscription = $stripeClient->cancelSubscription($this->gateway_id)) {
                    // Updates end date
                    if (isset($stripeSubscription->current_period_end)
                        && !empty($stripeSubscription->current_period_end)) {

                        $this->ends_at = $stripeSubscription->current_period_end;

                    } else {

                        $this->ends_at = $this->displayInfo('next_billing_time');

                        if (empty($this->ends_at)) {
                            $this->ends_at = time();
                        }
                    }
                    // Updates gateway status as cancelled
                    $this->gateway_status = $stripeSubscription->status;
                }
            } else {
                // Updates end date
                $this->ends_at = $this->displayInfo('next_billing_time');
                if (empty($this->ends_at)) {
                    $this->ends_at = time();
                }
                // Updates gateway status as cancelled
                $this->gateway_status = $stripeSubscription->status;
            }
        }
    }

    /**
     * Cancels Paypal Subscription
     */
    public function cancelPaypalSubscription()
    {
        $paypalClient = new PaypalClient();
        $paypalSubscription = $paypalClient->getSubscription($this->gateway_id);

        if (isset($paypalSubscription->status)) {
            if ($paypalSubscription->status !== 'CANCELLED') {
                if ($paypalClient->cancelSubscription($this->gateway_id)) {
                    // Updates end date
                    if (isset($paypalSubscription->billing_info->next_billing_time)
                        && !empty($paypalSubscription->billing_info->next_billing_time)) {

                        $this->ends_at = strtotime($paypalSubscription->billing_info->next_billing_time);

                    } else {

                        $this->ends_at = $this->displayInfo('next_billing_time');

                        if (empty($this->ends_at)) {
                            $this->ends_at = time();
                        }
                    }
                    // Updates gateway status as cancelled
                    $this->gateway_status = 'CANCELLED';
                }
            } else {
                // Updates end date
                $this->ends_at = $this->displayInfo('next_billing_time');
                if (empty($this->ends_at)) {
                    $this->ends_at = time();
                }
                // Updates gateway status as cancelled
                $this->gateway_status = 'CANCELLED';
            }
        }
    }

    /**
     * Return specific information
     *
     * @param $key
     * @return mixed|string
     */
    public function displayInfo($key)
    {
        if (isset($this->info[$key])) {
            return $this->info[$key];
        }

        return '';
    }

    /**
     * Get Next Subscription Information (Paypal Gateway)
     *
     * Used when the change of a subscription plan
     * will be effective in the next billing cycle
     *
     * @return array|null
     */
    public function getNextSubscription()
    {
        if (!empty($this->info['next_subscription']) && is_array($this->info['next_subscription'])) {
            return $this->info['next_subscription'];
        }

        return null;
    }

    /**
     * Get Currency Code
     *
     * @return mixed|string|null
     */
    public function getCurrencyCode()
    {
        $code = null;

        if (!empty($this->info['currency_code'])) {
            $code = $this->info['currency_code'];
        }

        return $code;
    }

    /**
     * Get Currency Code
     *
     * @return mixed|string|null
     */
    public function getCurrencySymbol()
    {

        if (!empty($this->info['currency_code'])) {
            $currency = strtoupper($this->info['currency_code']);
            return CurrencyHelper::getCurrencySymbolAsHtmlEntity($currency);
        }

        return null;
    }

    /**
     * Get Total Amount
     *
     * @return string|null
     */
    public function getAmount()
    {
        $amount = null;

        if (!empty($this->info['amount'])) {
            // Amount is in cents
            $amount = $this->info['amount'] / 100;
            // $amount = number_format($amount, 2);
            $amount = Yii::$app->formatter->asDecimal($amount);
        }

        return $amount;
    }

    /**
     * Is it a recurring payment?
     *
     * @return bool
     */
    public function isRecurring()
    {
        return $this->type === SubscriptionPrice::TYPE_RECURRING;
    }

    /**
     * Is it a one-time payment?
     *
     * @return bool
     */
    public function isOneTime()
    {
        return $this->type === SubscriptionPrice::TYPE_ONE_TIME;
    }

    /**
     * Is it a Paypal Subscription?
     *
     * @return bool
     */
    public function isPaypal()
    {
        return $this->gateway === self::GATEWAY_PAYPAL;
    }

    /**
     * Is it a Stripe Subscription?
     *
     * @return bool
     */
    public function isStripe()
    {
        return $this->gateway === self::GATEWAY_STRIPE;
    }

    /**
     * Update user roles
     *
     * @return bool
     * @throws \Exception
     */
    public function updateUserRoles()
    {
        // Exclude if User has this permission
        $excluded = Yii::$app->user->can('manageSite');

        if (!$excluded) {

            // Revoke and Assign auth roles
            $roles = $this->product->getRoles();
            $auth = Yii::$app->authManager;
            $auth->revokeAll($this->user_id);

            foreach ($roles as $role) {
                $authRole = $auth->getRole($role);
                $auth->assign($authRole, $this->user_id);
            }

            return true;
        }

        return false;
    }

    /**
     * Downgrade User Roles
     *
     * @return bool
     * @throws \Exception
     */
    public function downgradeUserRoles()
    {
        // Exclude if User has this permission
        $excluded = Yii::$app->user->can('manageSite');

        if (!$excluded) {

            // Revoke auth roles
            $auth = Yii::$app->authManager;
            $auth->revokeAll($this->user_id);

            // Assign default user role
            if ($defaultUserRole = Yii::$app->settings->get('app.defaultUserRole')) {
                $roles = $auth->getRoles();
                $roles = ArrayHelper::getColumn($roles, 'name');
                if (in_array($defaultUserRole, $roles)) {
                    $authRole = $auth->getRole($defaultUserRole);
                    $auth->assign($authRole, $this->user_id);
                }
                $this->downgraded_at = time();
                $this->save();
            }

            return true;
        }

        return false;
    }

    /**
     * Get Next Billing Time
     *
     * @return string|null
     * @throws \yii\base\InvalidConfigException
     */
    public function getNextBillingTime()
    {
        $value = null;

        if (!empty($this->info['next_billing_time'])) {
            $nextBillingTime = $this->info['next_billing_time'];
            if (is_integer($nextBillingTime)) {
                $format = $this->getDateFormat();
                $value = Yii::$app->formatter->asDate($nextBillingTime, $format);
            }
        }

        return $value;
    }
}
