<?php

namespace App\Actions;

use App\Models\Customer;
use App\Models\Operator;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Models\Subscription;
use App\Provisioning\Jobs\AdvanceRunJob;
use App\Services\Billing\PlanCatalogue;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Gives a customer a package without a payment.
 *
 * A grant is an order without money, not a second code path: this creates the
 * exact Order → ProvisioningRun → Subscription rows a Stripe purchase does —
 * same pipeline, same OpenSubscription — with `stripe_subscription_id` left
 * null and `price_cents` set to whatever the customer actually pays (0 for a
 * full gift, less than the catalogue price for a discount). Provisioning,
 * DowngradeCheck, seats, quotas, cancellation: all of it reads an ordinary
 * Subscription row afterwards and never learns this one was free.
 */
class GrantSubscription
{
    public function __construct(private OpenSubscription $openSubscription) {}

    /**
     * @param  array{quota_gb?:int,seats?:int,traffic_gb?:int}  $bonus  Beyond what the plan tier already includes.
     */
    public function __invoke(
        Customer $customer,
        Operator $grantedBy,
        string $plan,
        string $term,
        string $datacenter,
        int $priceCents,
        array $bonus = [],
        ?string $note = null,
        ?Carbon $until = null,
    ): Subscription {
        if (! Subscription::knowsPlan($plan)) {
            throw new RuntimeException("Plan '{$plan}' is not sellable, so it cannot be granted either.");
        }

        $version = app(PlanCatalogue::class)->currentVersion($plan);
        $cataloguePrice = $version->priceFor($term);

        if ($cataloguePrice === null) {
            throw new RuntimeException("Plan '{$plan}' is not sold on a {$term} term.");
        }

        // A "grant" that charges more than the plan actually costs is not a
        // grant — that path already exists, and it goes through Stripe.
        if ($priceCents < 0 || $priceCents > $cataloguePrice->amount_cents) {
            throw new RuntimeException('A grant cannot charge more than the catalogue price.');
        }

        [$run, $subscription] = DB::transaction(function () use (
            $customer, $grantedBy, $plan, $term, $datacenter, $priceCents, $bonus, $note, $until, $cataloguePrice,
        ) {
            $order = Order::create([
                'customer_id' => $customer->id,
                'plan' => $plan,
                'type' => 'new',
                'amount_cents' => $priceCents,
                'currency' => Subscription::catalogueCurrency(),
                'datacenter' => $datacenter,
                // Never Stripe's: this purchase never went through Stripe at
                // all, which is the whole point, and is also what tells
                // IssueInvoice and the revenue figures this row apart from an
                // ordinary (possibly also zero-priced) checkout.
                'stripe_event_id' => null,
                'stripe_subscription_id' => null,
                'status' => 'paid',
            ]);

            $run = ProvisioningRun::create([
                'subject_type' => Order::class,
                'subject_id' => $order->id,
                'pipeline' => 'customer',
                'status' => ProvisioningRun::STATUS_PENDING,
                'current_step' => 0,
                'context' => ['branding' => $customer->brandingResolved()],
            ]);

            $overrides = array_filter([
                'price_cents' => $priceCents,
                'quota_gb' => $bonus['quota_gb'] ?? null,
                'seats' => $bonus['seats'] ?? null,
                'traffic_gb' => $bonus['traffic_gb'] ?? null,
            ], fn ($value) => $value !== null);

            $overrides += [
                'granted_by' => $grantedBy->id,
                'granted_at' => now(),
                'grant_note' => $note,
                'granted_until' => $until,
                'catalogue_price_cents' => $cataloguePrice->amount_cents,
            ];

            $subscription = ($this->openSubscription)($order, $term, $overrides);

            return [$run, $subscription];
        });

        // After the commit, never inside it — a worker can pick the run up
        // before the transaction lands (see StartCustomerProvisioning).
        AdvanceRunJob::dispatch($run->uuid);

        return $subscription;
    }
}
