<?php

namespace App\Models;

use App\Models\Concerns\HasUuid;
use App\Observers\OrderObserver;
use App\Provisioning\Contracts\ProvisioningSubject;
use App\Services\Billing\TaxTreatment;
use Database\Factories\OrderFactory;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
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;

#[ObservedBy(OrderObserver::class)]
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', 'setup_fee_cents',
        'currency', 'datacenter', 'stripe_event_id', 'stripe_subscription_id', 'stripe_payment_intent_id',
        'stripe_invoice_id', 'status', 'immediate_start_consent_at',
    ];

    protected function casts(): array
    {
        return [
            'amount_cents' => 'integer',
            'setup_fee_cents' => 'integer',
            'plan_version_id' => 'integer',
            // When the consumer expressly asked for the service to begin inside
            // the withdrawal window.
            //
            // DEAD as a gate, kept as a record. It used to decide whether a
            // withdrawing consumer owed the pro-rata value of the days already
            // delivered; the owner has decided they owe nothing and get the whole
            // amount back, so nothing reads this to make a decision any more. It
            // stays because it is evidence of what the customer was shown and
            // agreed to at the checkout.
            'immediate_start_consent_at' => 'datetime',
        ];
    }

    /**
     * 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.
     *
     * Only ever asked of a PENDING cart line, whose `amount_cents` is a net
     * catalogue figure. An order that has been through a Stripe checkout already
     * holds what was taken; see chargedCents() below, which is what the invoice
     * uses and which does not double the tax on such an order.
     */
    public function grossCents(): int
    {
        return $this->taxTreatment()->grossCents($this->amount_cents);
    }

    /**
     * What this order actually cost the customer, whatever tax was in it.
     *
     * `amount_cents` means two things, and this is the one place that resolves
     * which. An order created from a Stripe checkout carries the session's
     * `amount_total` — what was actually taken from the card, whichever of a
     * package's two Prices this customer was sold on, so it needs nothing added to
     * it. An order that never went through a checkout (a grant, a line in the
     * portal cart) carries the catalogue's NET figure, and what a checkout would
     * have taken for it is that figure through this customer's own treatment: the
     * domestic gross where VAT is charged, the bare net for a reverse-charge
     * business.
     *
     * Keyed on `stripe_event_id` for exactly the reason OpenSubscription keys the
     * register's charged amount on it, and not on the amount being non-zero: a
     * fully discounted checkout charges nothing and is still a checkout.
     */
    public function chargedCents(): int
    {
        return $this->stripe_event_id !== null
            ? (int) $this->amount_cents
            : $this->taxTreatment()->chargeCents((int) $this->amount_cents);
    }

    /**
     * What was charged for the PACKAGE alone — the whole charge, less the one-off
     * setup fee that rode along on the same checkout.
     *
     * The two have to be separable because they answer to different things. A
     * withdrawal sends back everything that was taken and the confirmation mail
     * states everything that was taken, so both read `amount_cents` whole. The
     * proof register holds the charge against the contract's agreed price, and the
     * invoice prints the fee as a line of its own — neither of which works if the
     * fee is folded into the package's figure.
     *
     * Zero for every order that carries no fee, which is every order placed before
     * the fee was charged at all and every order that is not a first purchase: a
     * setup fee is charged once, when the cloud is set up.
     */
    public function packageChargedCents(): int
    {
        return $this->chargedCents() - (int) $this->setup_fee_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']);
    }

    public function provisioningPipeline(): string
    {
        return 'customer';
    }
}
