<?php

namespace App\Models;

use App\Models\Builders\AppendOnlyBuilder;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use RuntimeException;

/**
 * One commercial event, written once and never again.
 *
 * A register that can be edited proves nothing. So the model refuses updates
 * and deletes outright: a mistake is corrected by recording the correction,
 * which is how ledgers have always worked and why they can be trusted.
 */
class SubscriptionRecord extends Model
{
    use HasFactory;
    use HasUuids;

    public const EVENT_PURCHASE = 'purchase';

    public const EVENT_UPGRADE = 'upgrade';

    public const EVENT_DOWNGRADE = 'downgrade';

    /** A term that renewed and was paid. Stripe's invoice says so. */
    public const EVENT_RENEWAL = 'renewal';

    /** A renewal Stripe could not collect. Their dunning takes it from here. */
    public const EVENT_PAYMENT_FAILED = 'payment_failed';

    /**
     * A paid invoice that is not a cycle renewal — a proration, or a charge
     * raised by hand. Money received, so it belongs in the register, but it
     * does not start a new term and must not move the period.
     */
    public const EVENT_INVOICE_PAID = 'invoice_paid';

    public const EVENT_ADDON_BOOKED = 'addon_booked';

    public const EVENT_ADDON_CANCELLED = 'addon_cancelled';

    public const EVENT_CANCELLATION = 'cancellation';

    /**
     * A consumer exercised the fourteen-day right of withdrawal.
     *
     * Its own event and not a cancellation: a cancellation ends a contract that
     * was validly concluded and keeps every cent that was paid for the term, a
     * withdrawal unwinds the contract itself and sends money back. Filing the
     * two under one name would make the register unable to answer the one
     * question an auditor asks — how much was refunded, and why.
     *
     * `net_cents` is NEGATIVE on this event, like a cancelled module: it is
     * revenue leaving.
     */
    public const EVENT_WITHDRAWAL = 'withdrawal';

    /** The shape of the `snapshot` column. Bump when it changes. */
    public const SNAPSHOT_VERSION = 1;

    protected $guarded = [];

    protected static function booted(): void
    {
        static::updating(function () {
            throw new RuntimeException(
                'The proof register is append-only. Record a correcting event instead of editing history.'
            );
        });

        static::deleting(function () {
            throw new RuntimeException(
                'The proof register is append-only. A record that can be deleted is not evidence.'
            );
        });
    }

    protected function casts(): array
    {
        return [
            'net_cents' => 'integer',
            'tax_cents' => 'integer',
            'gross_cents' => 'integer',
            'tax_rate' => 'decimal:2',
            'reverse_charge' => 'boolean',
            'plan_version' => 'integer',
            'snapshot_version' => 'integer',
            'snapshot' => 'array',
            'occurred_at' => 'datetime',
        ];
    }

    public function uniqueIds(): array
    {
        return ['uuid'];
    }

    /**
     * The model events above only fire for instance operations. A bulk
     * `query()->update()` or `->delete()` would sail straight past them, so the
     * builder refuses those too.
     */
    public function newEloquentBuilder($query): AppendOnlyBuilder
    {
        return new AppendOnlyBuilder($query);
    }

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

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

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

    public function planVersion(): BelongsTo
    {
        return $this->belongsTo(PlanVersion::class, 'plan_version_id');
    }
}
