<?php

namespace App\Models;

use App\Models\Concerns\HasUuid;
use App\Provisioning\Contracts\ProvisioningSubject;
use App\Services\Billing\TaxTreatment;
use Database\Factories\OrderFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Order extends Model implements ProvisioningSubject
{
    /** @use HasFactory<OrderFactory> */
    use HasFactory, HasUuid;

    protected $fillable = [
        'customer_id', 'plan', 'plan_version_id', 'type', 'addon_key', 'amount_cents', 'currency',
        'datacenter', 'stripe_event_id', 'stripe_subscription_id', 'status',
    ];

    protected function casts(): array
    {
        return ['amount_cents' => 'integer', 'plan_version_id' => 'integer'];
    }

    /**
     * What this order actually buys, in words.
     *
     * Lives on the model rather than in the view: the cart, the invoice list and
     * any future confirmation mail must not each invent their own wording for
     * the same row.
     */
    public function label(): string
    {
        return match ($this->type) {
            'upgrade' => __('billing.cart.upgrade', ['plan' => ucfirst($this->plan)]),
            'storage' => __('billing.cart.storage', ['gb' => (int) config('provisioning.storage_addon.gb', 100)]),
            'traffic' => __('billing.cart.traffic', ['gb' => (int) config('provisioning.traffic.addon.gb', 1000)]),
            'addon' => __('billing.addon.'.$this->addon_key.'.name'),
            default => __('billing.cart.plan', ['plan' => ucfirst($this->plan)]),
        };
    }

    /**
     * Monthly or one-off.
     *
     * Traffic is bought for the month that is running out and does not renew;
     * everything else changes the recurring bill.
     */
    public function isRecurring(): bool
    {
        return $this->type !== 'traffic';
    }

    /**
     * Net is what is stored; gross is what gets charged — and how much VAT that
     * is depends on the customer, not on a global setting.
     */
    public function grossCents(): int
    {
        return $this->taxTreatment()->grossCents($this->amount_cents);
    }

    public function taxTreatment(): TaxTreatment
    {
        return TaxTreatment::for($this->customer);
    }

    /** Only a pending order is still the customer's to change. */
    public function isRemovable(): bool
    {
        return $this->status === 'pending';
    }

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

    public function instance(): HasOne
    {
        return $this->hasOne(Instance::class);
    }

    /** The contract this order opened, if it was the purchase that opened one. */
    public function subscription(): HasOne
    {
        return $this->hasOne(Subscription::class);
    }

    /** The module this order booked, if it was an add-on order. */
    public function subscriptionAddon(): HasOne
    {
        return $this->hasOne(SubscriptionAddon::class);
    }

    /**
     * A grant that costs nothing — the one case IssueInvoice must skip.
     *
     * Keyed on the linked subscription/add-on's OWN provenance, not merely on
     * `amount_cents === 0`: a fully discounted Stripe checkout also charges
     * zero and that invoice is still owed (OpenSubscription's docblock says
     * the same about the register). Only a grant with nothing charged, and
     * chosen as a full gift rather than a discount, produces no invoice.
     */
    public function isFreeGrant(): bool
    {
        if ((int) $this->amount_cents !== 0) {
            return false;
        }

        return $this->subscription?->isFreeGrant()
            ?? $this->subscriptionAddon?->isGranted()
            ?? false;
    }

    public function runs(): MorphMany
    {
        return $this->morphMany(ProvisioningRun::class, 'subject');
    }

    /**
     * Runner hook: a failed provisioning run marks the order failed (no
     * auto-refund) AND releases the reserved instance, so its quota stops
     * counting against host capacity.
     */
    public function onProvisioningFailed(): void
    {
        $this->update(['status' => 'failed']);
        $this->instance()->update(['status' => 'failed']);
    }
}
