<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

/**
 * Gives provisioning a way to reach the contract, and the contract everything
 * provisioning needs.
 *
 * Until now the pipeline re-read config/provisioning.php by the order's plan
 * name, so the snapshot protected the customer's price but not their machine:
 * shrinking a plan resized an existing customer's VM on the next run. The
 * pipeline's subject is the order, so the order is how it finds the snapshot.
 */
return new class extends Migration
{
    /**
     * The catalogue as it stood when this migration was written. Inlined, not
     * read from config: the plans array moved into its own tables one migration
     * later, and a migration that reads config stops working the moment config
     * moves on. These are the terms the orders being backfilled were sold under.
     */
    private const CATALOGUE_AT_THE_TIME = [
        'start' => ['tier' => 1, 'traffic_gb' => 1000, 'quota_gb' => 100, 'disk_gb' => 120, 'ram_mb' => 4096, 'cores' => 2, 'seats' => 5, 'performance' => 'standard', 'price_cents' => 4900, 'template_vmid' => 9000],
        'team' => ['tier' => 2, 'traffic_gb' => 3000, 'quota_gb' => 500, 'disk_gb' => 540, 'ram_mb' => 8192, 'cores' => 4, 'seats' => 25, 'performance' => 'enhanced', 'price_cents' => 17900, 'template_vmid' => 9000],
        'business' => ['tier' => 3, 'traffic_gb' => 8000, 'quota_gb' => 1000, 'disk_gb' => 1050, 'ram_mb' => 16384, 'cores' => 8, 'seats' => 100, 'performance' => 'high', 'price_cents' => 39900, 'template_vmid' => 9000],
        'enterprise' => ['tier' => 4, 'traffic_gb' => 20000, 'quota_gb' => 2000, 'disk_gb' => 2100, 'ram_mb' => 32768, 'cores' => 16, 'seats' => 500, 'performance' => 'dedicated', 'price_cents' => 79900, 'template_vmid' => 9000],
    ];

    public function up(): void
    {
        Schema::table('subscriptions', function (Blueprint $table) {
            // The order that opened this contract. Unique, because one purchase
            // opens one contract — later upgrades and add-ons are their own
            // orders against the same subscription, never new subscriptions.
            $table->foreignId('order_id')->nullable()->unique()->after('customer_id')
                ->constrained()->nullOnDelete();

            // The blueprint the customer's VM is cloned from. Frozen here so the
            // pipeline stops reading the live catalogue — but deliberately NOT
            // in Subscription::FROZEN: this is how we build the machine, not a
            // condition the customer is owed, and an operator has to be able to
            // point a contract at a replacement template when the old one is
            // gone, without cancelling the contract to do it.
            $table->unsignedInteger('template_vmid')->nullable()->after('performance');
        });

        $this->backfillContracts();
    }

    public function down(): void
    {
        Schema::table('subscriptions', function (Blueprint $table) {
            $table->dropConstrainedForeignId('order_id');
            $table->dropColumn('template_vmid');
        });

        // Backfilled contracts are deliberately left in place. Deleting a
        // customer's contract to undo a schema change would destroy the record
        // of what they are owed; a stale row is the lesser harm.
    }

    /**
     * Give every order that already bought something the contract it should
     * always have had.
     *
     * Provisioning now fails closed without one, so an order that is paid or
     * mid-flight when this deploys would otherwise stop dead at ValidateOrder.
     *
     * Everything is inlined rather than routed through Subscription — a
     * migration has to keep working when the model moves on.
     */
    private function backfillContracts(): void
    {
        $plans = self::CATALOGUE_AT_THE_TIME;
        $currency = strtoupper((string) config('provisioning.currency', 'EUR'));

        $orders = DB::table('orders')
            ->where('type', 'new')  // only a purchase opens a contract
            ->where(function ($query) {
                $query
                    ->whereIn('status', ['paid', 'provisioning', 'active'])
                    // A failed run does not undo a payment: onProvisioningFailed()
                    // marks the order failed and refunds nothing, and from now on
                    // such an order keeps the contract opened before provisioning
                    // ever started. Backfill matches that — but only where the
                    // money is evidenced, since an order that was never paid can
                    // end up failed too. Only the paid webhook sets
                    // stripe_event_id; the cart leaves it null and pending.
                    ->orWhere(fn ($paid) => $paid
                        ->where('status', 'failed')
                        ->whereNotNull('stripe_event_id'));
            })
            ->orderBy('id')
            ->get();

        foreach ($orders as $order) {
            if (DB::table('subscriptions')->where('order_id', $order->id)->exists()) {
                continue;
            }

            $plan = $plans[$order->plan] ?? null;

            if ($plan === null) {
                continue; // a plan we no longer sell: nothing left to reconstruct from
            }

            // A payment in a currency the catalogue cannot express gets no
            // contract, exactly as the checkout path refuses one — writing an
            // EUR price against a CHF payment would bake in a disagreement.
            if (strtoupper((string) $order->currency) !== $currency) {
                continue;
            }

            // What was actually delivered outranks what the catalogue says now:
            // the catalogue may already have drifted from what this customer was
            // sold, and the running machine is the honest record of their terms.
            $instance = DB::table('instances')->where('order_id', $order->id)->first();

            // A contract may already exist and simply have no order_id, because
            // nothing set one before this migration. Adopt it rather than open a
            // second: two active contracts for one purchase make "what is this
            // customer owed?" unanswerable, and its frozen terms are the real
            // ones — reconstructing them from the catalogue would be a guess.
            $orphan = DB::table('subscriptions')
                ->whereNull('order_id')
                ->where('customer_id', $order->customer_id)
                ->where('plan', $order->plan)
                ->when(
                    $instance !== null,
                    fn ($q) => $q->where(fn ($m) => $m->where('instance_id', $instance->id)->orWhereNull('instance_id')),
                    fn ($q) => $q->whereNull('instance_id'),
                )
                ->orderBy('id')
                ->first();

            if ($orphan !== null) {
                DB::table('subscriptions')->where('id', $orphan->id)->update(array_filter([
                    'order_id' => $order->id,
                    // Fill only what is missing. The snapshot itself is left
                    // untouched: it is what this customer was actually sold.
                    'instance_id' => $orphan->instance_id ?? $instance->id ?? null,
                    'template_vmid' => $orphan->template_vmid
                        ?? (isset($plan['template_vmid']) ? (int) $plan['template_vmid'] : null),
                    'updated_at' => now(),
                ], fn ($value) => $value !== null));

                continue;
            }

            // Anchor the term on the purchase and roll it forward to the period
            // that is running now, so the next renewal lands where it should.
            $start = Carbon::parse($order->created_at);
            $end = $start->copy()->addMonth();
            while ($end->isPast()) {
                $end->addMonth();
            }

            DB::table('subscriptions')->insert([
                'uuid' => (string) Str::uuid(),
                'customer_id' => $order->customer_id,
                'order_id' => $order->id,
                'instance_id' => $instance->id ?? null,
                'plan' => $order->plan,
                'tier' => (int) ($plan['tier'] ?? 0),
                'term' => 'monthly',
                'price_cents' => (int) ($plan['price_cents'] ?? 0),
                'currency' => $currency,
                'quota_gb' => (int) ($instance->quota_gb ?? $plan['quota_gb'] ?? 0),
                'traffic_gb' => (int) ($plan['traffic_gb'] ?? 0),
                'seats' => (int) ($plan['seats'] ?? 0),
                'ram_mb' => (int) ($instance->ram_mb ?? $plan['ram_mb'] ?? 0),
                'cores' => (int) ($instance->cores ?? $plan['cores'] ?? 0),
                'disk_gb' => (int) ($instance->disk_gb ?? $plan['disk_gb'] ?? 0),
                'performance' => $plan['performance'] ?? null,
                'template_vmid' => isset($plan['template_vmid']) ? (int) $plan['template_vmid'] : null,
                'started_at' => $start,
                'current_period_start' => $end->copy()->subMonth(),
                'current_period_end' => $end,
                'status' => 'active',
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }
};
