<?php namespace Common\Billing;

use App\Models\User;
use Common\Billing\Gateways\Contracts\CommonSubscriptionGatewayActions;
use Common\Billing\Gateways\Paypal\Paypal;
use Common\Billing\Gateways\Stripe\Stripe;
use Common\Billing\Invoices\Invoice;
use Common\Billing\Models\Price;
use Common\Billing\Models\Product;
use Common\Billing\Subscriptions\SubscriptionFactory;
use Common\Core\BaseModel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use LogicException;

class Subscription extends BaseModel
{
    use HasFactory;

    public const MODEL_TYPE = 'subscription';

    protected $guarded = ['id'];

    protected $appends = [
        'on_grace_period',
        'past_due',
        'on_trial',
        'valid',
        'active',
        'cancelled',
    ];

    protected $casts = [
        'id' => 'integer',
        'price_id' => 'integer',
        'product_id' => 'integer',
        'quantity' => 'integer',
        'trial_ends_at' => 'datetime',
        'ends_at' => 'datetime',
        'renews_at' => 'datetime',
    ];

    public function getOnGracePeriodAttribute(): bool
    {
        return $this->onGracePeriod();
    }

    public function getOnTrialAttribute(): bool
    {
        return $this->onTrial();
    }

    public function getPastDueAttribute(): bool
    {
        return $this->gateway()?->isSubscriptionPastDue($this) ?? false;
    }

    public function getValidAttribute(): bool
    {
        return $this->valid();
    }

    public function getActiveAttribute(): bool
    {
        return $this->active();
    }

    public function getCancelledAttribute(): bool
    {
        return $this->cancelled();
    }

    public function invoices(): HasMany
    {
        return $this->hasMany(Invoice::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function price(): BelongsTo
    {
        return $this->belongsTo(Price::class);
    }

    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class);
    }

    public function onTrial(): bool
    {
        return $this->trial_ends_at?->isFuture() ?? false;
    }

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

    public function ended(): bool
    {
        return $this->cancelled() && !$this->onGracePeriod();
    }

    public function active(): bool
    {
        if ($this->ended()) {
            return false;
        }

        // first payment failed, don't show billing page and allow opening checkout routes
        $gateway = $this->gateway();
        if ($gateway?->isSubscriptionIncomplete($this)) {
            return false;
        }

        return !$this->ended();
    }

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

    /**
     * Determine if the subscription is within its grace period after cancellation.
     */
    public function onGracePeriod(): bool
    {
        return $this->ends_at?->isFuture() ?? false;
    }

    public function changePlan(Product $newProduct, Price $newPrice): self
    {
        $isSuccess = $this->gateway()?->changePlan(
            $this,
            $newProduct,
            $newPrice,
        );

        if ($isSuccess) {
            $this->fill([
                'product_id' => $newProduct->id,
                'price_id' => $newPrice->id,
                'ends_at' => null,
            ])->save();
        }

        return $this;
    }

    public function cancel(bool $atPeriodEnd = true): self
    {
        if ($this->gateway_name !== 'none') {
            $this->gateway()->cancelSubscription($this, $atPeriodEnd);
        }

        // 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
        // and make that the end of the grace period for this current user.
        if ($this->onTrial()) {
            $this->ends_at = $this->trial_ends_at;
        } else {
            $this->ends_at = $this->renews_at;
        }

        $this->renews_at = null;
        $this->save();

        return $this;
    }

    /**
     * Mark subscription as cancelled on local database
     * only, without interacting with payment gateway.
     */
    public function markAsCancelled(): void
    {
        $this->fill([
            'ends_at' => $this->renews_at,
            'renews_at' => null,
        ])->save();
    }

    /**
     * Cancel the subscription immediately and delete it from database.
     */
    public function cancelAndDelete(): self
    {
        $this->cancel(false);
        $this->delete();
        $this->invoices()->delete();

        $this->user->update([
            'card_last_four' => null,
            'card_brand' => null,
            'card_expires' => null,
        ]);

        return $this;
    }

    public function resume(): self
    {
        if (!$this->onGracePeriod()) {
            throw new LogicException(
                __(
                    'Unable to resume subscription that is not within grace period.',
                ),
            );
        }

        if ($this->onTrial()) {
            $trialEnd = $this->trial_ends_at->getTimestamp();
        } else {
            $trialEnd = 'now';
        }

        // To resume the subscription we need to set the plan parameter on the Stripe
        // subscription object. This will force Stripe to resume this subscription
        // where we left off. Then, we'll set the proper trial ending timestamp.
        if ($this->gateway_name !== 'none') {
            $this->gateway()->resumeSubscription($this, [
                'trial_end' => $trialEnd,
            ]);
        }

        // Finally, we will remove the ending timestamp from the user's record in the
        // local database to indicate that the subscription is active again and is
        // no longer "cancelled". Then we will save this record in the database.
        $this->renews_at = $this->ends_at;
        $this->ends_at = null;
        $this->save();

        return $this;
    }

    /**
     * Get gateway this subscription was created with.
     */
    public function gateway(): ?CommonSubscriptionGatewayActions
    {
        if ($this->gateway_name === 'stripe') {
            return app(Stripe::class);
        } elseif ($this->gateway_name === 'paypal') {
            return app(Paypal::class);
        }

        return null;
    }

    public function toNormalizedArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->this->gateway_name,
            'model_type' => self::MODEL_TYPE,
        ];
    }

    public function toSearchableArray(): array
    {
        return [
            'id' => $this->id,
            'product_id' => $this->product_id,
            'price_id' => $this->price_id,
            'gateway_name' => $this->gateway_name,
            'user' => $this->user ? $this->user->getSearchableValues() : null,
            'description' => $this->description,
            'ends_at' => $this->ends_at,
            'created_at' => $this->created_at->timestamp ?? '_null',
            'updated_at' => $this->updated_at->timestamp ?? '_null',
        ];
    }

    protected function makeAllSearchableUsing($query)
    {
        return $query->with(['user']);
    }

    public static function filterableFields(): array
    {
        return [
            'id',
            'product_id',
            'price_id',
            'gateway_name',
            'ends_at',
            'created_at',
            'updated_at',
        ];
    }

    protected static function newFactory()
    {
        return SubscriptionFactory::new();
    }

    public static function getModelTypeAttribute(): string
    {
        return self::MODEL_TYPE;
    }
}
