<?php

namespace App\Models;

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;

/**
 * What a version costs for one term.
 *
 * One row per version AND term, each with its own Stripe Price id — a Stripe
 * Price carries its own recurring interval, so monthly and yearly can never
 * share one. Amounts are NET; what is actually charged depends on the
 * customer's tax treatment.
 *
 * Frozen with its version. Repricing means publishing a new version, which is
 * what Stripe requires anyway — a Stripe Price is immutable, so a rise mints a
 * new one and existing subscriptions keep the old.
 *
 * The reason is not tidiness. A checkout is not instant: the session opens, the
 * customer types their card details, the webhook arrives. An amount edited in
 * that gap would contract them at a price they were never quoted, while their
 * payment carries the old one. Only `stripe_price_id` stays writable, because
 * it is filled in after the fact.
 */
class PlanPrice extends Model
{
    use HasFactory;
    use HasUuids;

    /** What publishing the version nails down. */
    public const FROZEN = ['plan_version_id', 'term', 'amount_cents', 'currency'];

    protected $guarded = [];

    protected static function booted(): void
    {
        static::updating(function (self $price) {
            // A draft is still a draft: nothing has been quoted to anyone.
            if ($price->version?->isPublished() !== true) {
                return;
            }

            $frozen = array_intersect(array_keys($price->getDirty()), self::FROZEN);

            if ($frozen !== []) {
                throw new RuntimeException(
                    'The price of a published plan version is fixed; tried to change: '.implode(', ', $frozen).
                    '. Publish a new version instead — someone may be at the checkout looking at this one.'
                );
            }
        });

        static::deleting(function (self $price) {
            // Same loophole as editing. Remove the price of a version someone is
            // checking out on, and their payment lands on a plan that can no
            // longer be priced — the order is recorded, no contract opens, and
            // they are left paid-for and unprovisioned.
            if ($price->version?->isPublished() === true) {
                throw new RuntimeException(
                    'The price of a published plan version cannot be deleted. '.
                    'Close the version\'s availability window instead.'
                );
            }
        });
    }

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

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

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