<?php

namespace App\Provisioning\Steps\Customer;

use App\Models\Host;
use App\Models\Instance;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class ReserveResources extends CustomerStep
{
    public function key(): string
    {
        return 'reserve_resources';
    }

    public function maxDuration(): int
    {
        return 60;
    }

    public function execute(ProvisioningRun $run): StepResult
    {
        $order = $this->order($run);

        // Idempotent: an instance already exists for this order.
        $existing = Instance::query()->where('order_id', $order->id)->first();
        if ($existing !== null) {
            $this->putContext($run, $existing);
            $this->linkToSubscription($run, $existing);

            return StepResult::advance();
        }

        $subscription = $this->subscription($run);

        if ($subscription === null) {
            return StepResult::fail('no_subscription');
        }

        // Sizes come from the contract, not the catalogue: this customer gets
        // the machine they paid for even if the plan was edited since.
        $plan = $this->plan($run);

        // Place + create under a per-datacenter lock so concurrent reservations
        // can't both pick the same host and overcommit its capacity.
        $instance = Cache::lock('placement:'.$order->datacenter, 30)->block(10, function () use ($order, $plan) {
            $host = Host::placeableIn($order->datacenter, (int) $plan['disk_gb']);
            if ($host === null) {
                return null;
            }

            return Instance::create([
                'customer_id' => $order->customer_id,
                'order_id' => $order->id,
                'host_id' => $host->id,
                'plan' => $order->plan,
                'quota_gb' => $plan['quota_gb'],
                'disk_gb' => $plan['disk_gb'],
                'ram_mb' => $plan['ram_mb'],
                'cores' => $plan['cores'],
                'subdomain' => $this->uniqueSubdomain($order),
                'status' => 'reserving',
            ]);
        });

        if ($instance === null) {
            return StepResult::fail('no_capacity');
        }

        $this->putContext($run, $instance);
        $this->linkToSubscription($run, $instance);
        $this->recordResource($run, $instance->host, 'instance_id', (string) $instance->id);

        return StepResult::advance();
    }

    /**
     * Point the contract at the machine that fulfils it, so anything asking
     * "what is this customer entitled to?" can get there from either end.
     * instance_id is not part of the frozen snapshot, so this is allowed.
     */
    private function linkToSubscription(ProvisioningRun $run, Instance $instance): void
    {
        $subscription = $this->subscription($run);

        if ($subscription !== null && $subscription->instance_id !== $instance->id) {
            $subscription->update(['instance_id' => $instance->id]);
        }
    }

    private function putContext(ProvisioningRun $run, Instance $instance): void
    {
        $run->mergeContext([
            'instance_id' => $instance->id,
            'host_id' => $instance->host_id,
            'node' => $instance->host?->node ?? 'pve',
            'subdomain' => $instance->subdomain,
            'plan' => $instance->plan,
        ]);
    }

    private function uniqueSubdomain(Order $order): string
    {
        $base = Str::slug($order->customer->name) ?: 'cloud';

        do {
            $subdomain = $base.'-'.Str::lower(Str::random(5));
        } while (Instance::query()->where('subdomain', $subdomain)->exists());

        return $subdomain;
    }
}
