<?php

namespace App\Provisioning\Steps\Customer;

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

/**
 * The machine is back — check that it came back as the machine that was sold,
 * and only then stop saying a restart is due.
 *
 * `restart_required_since` is the one thing a customer and an operator both
 * read to answer "am I paying for cores I do not have". Clearing it because
 * somebody pressed a button, or because a run reached its last step, would turn
 * it from a fact into a rumour — and the rumour points the wrong way, because it
 * says the problem is gone.
 *
 * So the proof is taken from the machine. Proxmox's status/current reports what
 * the RUNNING guest has (`cpus`, `maxmem`), not what its definition says, and
 * those two disagree for exactly as long as a pending restart lasts. If the
 * guest is back with at least what the instance row promises, the flag goes; if
 * it is back smaller, something did not take and the run fails loudly rather
 * than quietly declaring success.
 *
 * Where an older Proxmox reports neither figure, the fallback is the pipeline
 * itself: this step only runs after ShutDownVirtualMachine saw the guest
 * genuinely stopped and StartVirtualMachine started it again, and a stopped
 * machine reads its definition when it starts. That is the same reasoning
 * ResizeVirtualMachine already uses to decide there is nothing pending on a
 * stopped guest.
 */
class CompleteRestart extends CustomerStep
{
    /** How long the guest may take to show up as running before this gives up. */
    private const RUNNING_DEADLINE = 120;

    public function __construct(private ProxmoxClient $pve) {}

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

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

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

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

        $node = (string) $run->context('node');
        $vmid = (int) $run->context('vmid');
        $status = $this->pve->forHost($instance->host)->vmStatus($node, $vmid);

        if (($status['status'] ?? '') !== 'running') {
            if ($run->started_at !== null && $run->started_at->copy()->addSeconds(self::RUNNING_DEADLINE)->isPast()) {
                return StepResult::fail('restart_did_not_come_back');
            }

            return StepResult::poll(5, 'waiting for the machine to come back');
        }

        if (($short = $this->shortfall($status, $instance->cores, $instance->ram_mb)) !== null) {
            return StepResult::fail('restart_applied_nothing: '.$short);
        }

        // Only here. The machine is running, its guest agent has answered (the
        // step before this one), and it is running on what was sold.
        if ($instance->restartIsPending()) {
            $instance->update(['restart_required_since' => null]);
        }

        $run->events()->create([
            'step' => $this->key(),
            'attempt' => $run->attempt,
            'outcome' => 'info',
            'message' => "Die Maschine läuft wieder mit {$instance->cores} vCPU und {$instance->ram_mb} MB RAM. "
                .'Der offene Neustart ist damit erledigt.',
        ]);

        return StepResult::advance();
    }

    /**
     * What the running guest is short of, or null when it is not short of
     * anything we can see.
     *
     * Compared with `>=` rather than `===`: Proxmox rounds memory and a host may
     * legitimately give a guest more than was asked for, and refusing a machine
     * for being too big would be a strange way to protect a customer.
     *
     * @param  array<string, mixed>  $status
     */
    private function shortfall(array $status, ?int $cores, ?int $ramMb): ?string
    {
        if (isset($status['cpus']) && $cores > 0 && (int) $status['cpus'] < $cores) {
            return "{$status['cpus']} statt {$cores} vCPU";
        }

        if (isset($status['maxmem']) && $ramMb > 0) {
            $actualMb = (int) ((int) $status['maxmem'] / 1048576);

            if ($actualMb < $ramMb) {
                return "{$actualMb} statt {$ramMb} MB RAM";
            }
        }

        return null;
    }
}
