<?php

namespace App\Provisioning\Steps\Host;

use App\Models\PlanVersion;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Proxmox\ProxmoxClient;
use Throwable;

/**
 * Confirms the VM templates the plan catalogue sells actually exist on this node.
 *
 * Without this, a host went `active` while it could not serve a single customer.
 * Every plan version points at a `template_vmid`, nothing in the host pipeline
 * creates, copies or verifies that template, and there is no console command that
 * does either. The first paid order placed on a freshly onboarded host therefore
 * died in CloneVirtualMachine — after the customer had paid, in the one place
 * where nobody is watching and the only honest recovery is a refund.
 *
 * So the gap is reported HERE, during onboarding, with an operator in front of
 * the run. This step deliberately does NOT build a template: what goes into the
 * golden image (Nextcloud version, cloud-init, guest agent, disk layout) is a
 * product decision, and a step that invented one would produce customers running
 * something nobody chose.
 *
 * Placed straight after VerifyProxmoxApi because it is the first thing worth
 * asking the API once the API answers, and before RegisterCapacity so a host that
 * cannot serve anyone never reaches the capacity table at all.
 */
class VerifyVmTemplate extends HostStep
{
    public function __construct(private ProxmoxClient $pve) {}

    public function key(): string
    {
        return 'verify_vm_template';
    }

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

        // A catalogue with nothing published yet requires no template. This is a
        // real state — an installation being set up for the first time onboards
        // its host before it publishes its plans — and failing here would be
        // inventing a requirement nobody has stated.
        if ($required === []) {
            return StepResult::advance();
        }

        $client = $this->pve->forHost($host);

        try {
            $nodes = $client->listNodes();
            $node = $nodes[0]['node'] ?? 'pve';

            $missing = array_values(array_filter(
                $required,
                fn (int $vmid) => ! $client->vmExists($node, $vmid),
            ));
        } catch (Throwable $e) {
            // The API was answering one step ago; a hiccup here is transient.
            return StepResult::retry(15, 'could not ask Proxmox about the VM templates: '.$e->getMessage());
        }

        if ($missing !== []) {
            return StepResult::fail(
                'This host has no VM template to clone from, so it could not serve a single order. Missing on node '.
                $node.': VMID '.implode(', ', $missing).', which '.
                (count($missing) === 1 ? 'a published plan version points' : 'published plan versions point').' at. '.
                'Copy the golden template onto this host (or restore it from another node) and retry — otherwise the '.
                'first paid order placed here fails in clone_vm, after the customer has paid.'
            );
        }

        return StepResult::advance();
    }

    /**
     * Every template a currently sellable plan version points at.
     *
     * Published versions whose window has not closed, rather than
     * PlanVersion::available() — a version scheduled to launch next week is not
     * "available" yet and its template gap would then surface at its first order
     * instead of here, which is the whole failure this step removes. A closed
     * window is different: nobody can buy it any more, so its template is not
     * this host's problem.
     *
     * @return array<int, int>
     */
    private function requiredTemplateVmids(): array
    {
        return PlanVersion::query()
            ->whereNotNull('published_at')
            ->whereNotNull('template_vmid')
            ->where(fn ($q) => $q->whereNull('available_until')->orWhere('available_until', '>', now()))
            ->pluck('template_vmid')
            ->map(fn ($vmid) => (int) $vmid)
            ->unique()
            ->sort()
            ->values()
            ->all();
    }
}
