<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
 * @method static where(string $string, string $uid)
 * @method static count()
 * @method static offset(mixed $start)
 * @method static whereLike(string[] $array, mixed $search)
 * @method static insert(array[] $subscriptions)
 * @method static whereNull(string $string)
 * @method static whereDate(string $string, string $toDateString)
 * @property mixed options
 * @property mixed status
 * @property mixed end_at
 * @property int|mixed end_by
 * @property Carbon|mixed start_at
 * @property bool|mixed|null current_period_ends_at
 * @property mixed end_period_last_days
 * @property mixed plan
 * @property mixed payment_claimed
 */
class Subscription extends Model
{

    const STATUS_NEW     = 'new';
    const STATUS_PENDING = 'pending';
    const STATUS_ACTIVE  = 'active';
    const STATUS_ENDED   = 'ended';
    const STATUS_RENEW   = 'renew';

    /**
     * The attributes that should be mutated to date.
     *
     * @var array
     */
    protected $casts = [
            'current_period_ends_at' => 'datetime',
            'start_at'               => 'datetime',
            'end_at'                 => 'datetime',
            'created_at'             => 'datetime',
            'updated_at'             => 'datetime',
    ];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
            'uid',
            'options',
            'status',
            'paid',
            'current_period_ends_at',
            'payment_claimed',
            'start_at',
            'end_at',
            'end_period_last_days',
    ];


    /**
     * Indicates if the plan change should be prorated.
     *
     * @var bool
     */
    protected $prorate = true;


    /**
     * Bootstrap any application services.
     */
    public static function boot()
    {
        parent::boot();

        // Create uid when creating list.
        static::creating(function ($subscription) {
            // Create new uid
            $uid = uniqid();
            while (self::where('uid', $uid)->count() > 0) {
                $uid = uniqid();
            }
            $subscription->uid     = $uid;
            $subscription->options = json_encode([
                    'credit_warning'       => true,
                    'credit'               => '100',
                    'credit_notify'        => 'both',
                    'subscription_warning' => true,
                    'subscription_notify'  => 'both',
                    'send_warning'         => false,
            ]);
        });
    }

    /**
     * Find item by uid.
     *
     * @param $uid
     *
     * @return object
     */
    public static function findByUid($uid): object
    {
        return self::where('uid', $uid)->first();
    }


    /**
     * Get options.
     *
     * @return mixed
     */
    public function getOptions()
    {
        if ( ! $this->options) {
            return json_decode('{}', true);
        }

        return json_decode($this->options, true);
    }

    /**
     * get single option
     *
     * @param $name
     *
     * @return string
     */
    public function getOption($name): string
    {
        return $this->getOptions()[$name];
    }


    /**
     * update options
     *
     * @param $data
     *
     */
    public function updateOptions($data)
    {
        $options       = (object) array_merge((array) $this->getOptions(), $data);
        $this->options = json_encode($options);

        $this->save();
    }


    /**
     * plan associations
     *
     * @return BelongsTo
     *
     */
    public function plan(): BelongsTo
    {
        return $this->belongsTo(Plan::class, 'plan_id', 'id');
    }

    /**
     * Get the user that owns the subscription.
     *
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id', 'id');
    }

    /**
     * subscription transactions associations
     *
     * @return HasMany
     */
    public function subscriptionTransactions(): HasMany
    {

        return $this->hasMany(SubscriptionTransaction::class);
    }

    /**
     * subscriptions logs associations
     *
     * @return HasMany
     */
    public function subscriptionLogs(): HasMany
    {
        return $this->hasMany(SubscriptionLog::class);
    }


    /**
     * Determine if the subscription is active.
     *
     * @return bool
     */
    public function isActive(): bool
    {
        return $this->status == self::STATUS_ACTIVE;
    }

    /**
     * Determine if the subscription is active.
     *
     * @return bool
     */
    public function isNew(): bool
    {
        return $this->status == self::STATUS_NEW;
    }

    /**
     * Determine if the subscription is renewing.
     *
     * @return bool
     */
    public function isRenew(): bool
    {
        return $this->status == self::STATUS_RENEW;
    }

    /**
     * Determine if the subscription is active.
     *
     * @return bool
     */
    public function isPending(): bool
    {
        return $this->status == self::STATUS_PENDING;
    }


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

    /**
     * Determine if the subscription is ended.
     *
     * @return bool
     */
    public function isEnded(): bool
    {
        return $this->status == self::STATUS_ENDED;
    }

    /**
     * Cancel subscription. Set ends at to the end of period.
     *
     * @return void
     */
    public function cancelNow()
    {
        $this->setEnded();
    }

    /**
     * Determine if the subscription is ended.
     *
     * @param  int  $ended_by
     *
     * @return void
     */
    public function setEnded(int $ended_by = 1)
    {

        $invoice = Invoices::whereLike(['transaction_id'], $this->uid)->first();
        $invoice?->delete();

        $this->status                 = self::STATUS_ENDED;
        $this->end_by                 = $ended_by;
        $this->current_period_ends_at = Carbon::now();
        $this->end_at                 = Carbon::now();
        $this->save();
    }


    /**
     * Determine if the subscription is pending.
     *
     * @return void
     */
    public function setPending()
    {
        $this->status = self::STATUS_PENDING;
        $this->save();
    }

    /**
     * Determine if the subscription is pending.
     *
     * @return void
     */
    public function setActive()
    {
        $this->status                 = self::STATUS_ACTIVE;
        $this->start_at               = Carbon::now();
        $this->current_period_ends_at = $this->getPeriodEndsAt(Carbon::now());
        $this->save();
    }

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

    /**
     * Determine if the subscription is active.
     *
     * @return bool
     */
    public function active(): bool
    {
        return (is_null($this->end_at) || $this->onGracePeriod()) && ! $this->isPending() && ! $this->isNew();
    }

    /**
     * Determine if the subscription is recurring and not on trial.
     *
     * @return bool
     */
    public function isRecurring(): bool
    {
        return ! $this->cancelled();
    }

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

    /**
     * Check if subscription is going to expire.
     *
     * @return Boolean
     */
    public function goingToExpire(): bool
    {
        if ( ! $this->end_at) {
            return false;
        }

        $days = $this->end_period_last_days;

        return $this->end_at->subDay($days)->lessThanOrEqualTo(Carbon::now());
    }


    /**
     *  Next one period to subscription.
     *
     * @return null
     */
    public function nextPeriod()
    {
        $endsAt = $this->current_period_ends_at;

        $interval      = $this->plan->getBillableInterval();
        $intervalCount = $this->plan->getBillableIntervalCount();

        match ($interval) {
            'month' => $endsAt->addMonthsNoOverflow($intervalCount),
            'day'   => $endsAt->addDay($intervalCount),
            'week'  => $endsAt->addWeek($intervalCount),
            'year'  => $endsAt->addYearsNoOverflow($intervalCount),
            default => null,
        };

        return $endsAt;
    }

    /**
     * Next one period to subscription.
     *
     * @return null
     */
    public function periodStartAt()
    {
        $startAt       = $this->current_period_ends_at;
        $interval      = $this->plan->getBillableInterval();
        $intervalCount = $this->plan->getBillableIntervalCount();

        match ($interval) {
            'month' => $startAt->subMonthsNoOverflow($intervalCount),
            'day'   => $startAt->subDay($intervalCount),
            'week'  => $startAt->subWeek($intervalCount),
            'year'  => $startAt->subYearsNoOverflow($intervalCount),
            default => null,
        };

        return $startAt;
    }

    /**
     * Add one period to subscription.
     *
     * @return void
     */
    public function addPeriod()
    {
        $this->end_at = $this->nextPeriod();
        $this->save();
    }

    /**
     *  Check if payment is claimed.
     *
     * @return mixed
     */
    public function isPaymentClaimed()
    {
        return $this->payment_claimed;
    }

    /**
     * Claim payment.
     *
     * @return void
     */
    public function claimPayment()
    {
        $this->payment_claimed = true;
        $this->save();
    }


    /**
     *
     * @param $startDate
     *
     * @return null
     */
    public function getPeriodEndsAt($startDate)
    {
        // does not support recurring, update ends at column
        $interval      = $this->plan->getBillableInterval();
        $intervalCount = $this->plan->getBillableIntervalCount();

        match ($interval) {
            'month' => $startDate->addMonthsNoOverflow($intervalCount),
            'day'   => $startDate->addDay($intervalCount),
            'week'  => $startDate->addWeek($intervalCount),
            'year'  => $startDate->addYearsNoOverflow($intervalCount),
            default => null,
        };

        return $startDate;
    }

    /**
     * Start subscription.
     *
     * @return void
     */
    public function start()
    {
        $this->end_at                 = null;
        $this->current_period_ends_at = $this->getPeriodEndsAt(Carbon::now());
        $this->status                 = self::STATUS_ACTIVE;
        $this->start_at               = Carbon::now();
        $this->save();
    }

    /**
     * Subscription transactions.
     *
     * @return Collection
     */
    public function getTransactions(): Collection
    {
        return $this->subscriptionTransactions()->orderBy('created_at', 'desc')->get();
    }

    /**
     * Subscription transactions.
     *
     * @return Collection
     */
    public function getLogs(): Collection
    {
        return $this->subscriptionLogs()->orderBy('created_at', 'desc')->get();
    }

    /**
     * Subscription transactions.
     *
     * @param $type
     * @param $data
     *
     * @return SubscriptionTransaction
     */
    public function addTransaction($type, $data): SubscriptionTransaction
    {
        $transaction                  = new SubscriptionTransaction();
        $transaction->subscription_id = $this->id;
        $transaction->type            = $type;
        $transaction->fill($data);

        if (isset($data['options'])) {
            $transaction->options = json_encode($data['options']);
        }

        $transaction->save();

        return $transaction;
    }

    /**
     * Subscription transactions.
     *
     * @param $type
     * @param $data
     * @param  null  $transaction_id
     *
     * @return SubscriptionLog
     */
    public function addLog($type, $data, $transaction_id = null): SubscriptionLog
    {
        $log                  = new SubscriptionLog();
        $log->subscription_id = $this->id;
        $log->type            = $type;
        $log->transaction_id  = $transaction_id;
        $log->save();

        if (isset($data)) {
            $log->updateData($data);
        }

        return $log;
    }


    /**
     * get route key by uid
     *
     * @return string
     */
    public function getRouteKeyName(): string
    {
        return 'uid';
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->name;
    }
}
