<?php

namespace App\Provisioning\Steps\Host;

use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Ssh\RemoteShell;

/**
 * Creates the automation role + user + API token on the host via `pveum` over
 * SSH. A fresh host has no API token yet, so this bootstrap must run through the
 * authenticated root SSH session, not the (token-less) REST API. The secret is
 * shown only once, so the token id is persisted before advancing; a re-run
 * deletes and re-mints the token to stay idempotent after a crash.
 */
class CreateAutomationToken extends HostStep
{
    public function __construct(private RemoteShell $shell) {}

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

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

        $role = (string) config('provisioning.proxmox.role_id');
        $privs = (string) config('provisioning.proxmox.role_privs');
        $user = (string) config('provisioning.proxmox.user');
        $tokenName = (string) config('provisioning.proxmox.token_name');

        $this->keyLogin($this->shell, $host);

        // The role is converged BEFORE the token short-circuit below, deliberately.
        //
        // `pveum role add … || true` only ever applied the privilege list on the
        // run that first created the role, so adding a privilege to
        // provisioning.role_privs reached new hosts and no existing one. That is
        // how Sys.Modify would have been missing on every host onboarded before
        // it was added, with the symptom appearing much later and somewhere else
        // entirely: a 403 from POST /cluster/backup failing a paying customer's
        // provisioning at register_backup. `role modify` without -append REPLACES
        // the list, so this line makes the host match the config rather than
        // accumulating whatever any past version of it once granted.
        // Two separate commands rather than one `add || modify` line, so a
        // failure can be attributed: `add` failing means the role is already
        // there, which is the ordinary case and not an error.
        $roleArgs = escapeshellarg($role).' -privs '.escapeshellarg($privs);
        if (! $this->shell->run('pveum role add '.$roleArgs)->ok()
            && ! ($modify = $this->shell->run('pveum role modify '.$roleArgs))->ok()) {
            // Der Grund von `pveum`, nicht nur die Feststellung, dass es nicht
            // ging. Fünf Wiederholungen mit „could not converge the Proxmox
            // automation role privileges" sagten, WAS scheiterte, und
            // verschwiegen das Einzige, was weiterhilft: welches Privileg diese
            // Proxmox-Fassung nicht kennt. Der Betreiber musste sich auf den
            // Host melden und den Befehl von Hand nachstellen, um zu erfahren,
            // was in der Zeile daneben schon gestanden hatte.
            return StepResult::retry(20, trim(
                'could not converge the Proxmox automation role privileges: '
                .$this->firstLineOf($modify->stderr ?: $modify->stdout)
            ));
        }

        // Idempotent: token already minted and persisted. Below the role
        // convergence, so a replay still repairs the privileges.
        if ($this->hasResource($run, 'pve_token') && filled($host->api_token_ref)) {
            return StepResult::advance();
        }

        // User and ACL are idempotent and have nothing to converge (unlike the
        // role, whose CONTENT changes as the pipeline grows), so "already exists"
        // stays tolerated here.
        $this->shell->run('pveum user add '.escapeshellarg($user).' || true');
        $this->shell->run('pveum acl modify / -user '.escapeshellarg($user).' -role '.escapeshellarg($role).' || true');

        // Drop any half-created token from a prior crashed attempt, then mint fresh.
        $this->shell->run('pveum user token remove '.escapeshellarg($user).' '.escapeshellarg($tokenName).' || true');
        $result = $this->shell->run(
            'pveum user token add '.escapeshellarg($user).' '.escapeshellarg($tokenName).' -privsep 0 --output-format json'
        );

        if (! $result->ok()) {
            return StepResult::retry(20, 'Proxmox token creation failed');
        }

        $data = json_decode($result->stdout, true);
        $secret = $data['value'] ?? null;

        if (blank($secret)) {
            return StepResult::retry(20, 'Proxmox token secret was empty');
        }

        $tokenId = $data['full-tokenid'] ?? "{$user}!{$tokenName}";

        $host->update(['api_token_ref' => $tokenId.'='.$secret]);
        $this->recordResource($run, $host, 'pve_token', $tokenId);

        return StepResult::advance();
    }

    /**
     * Die erste Zeile einer Fehlerausgabe, gekürzt.
     *
     * Die erste, weil `pveum` den Grund voranstellt und danach seine
     * Aufrufhilfe ausbreitet — die ganze Ausgabe wäre ein Bildschirm Text in
     * einer Ereigniszeile. Gekürzt, weil diese Zeile in einer Tabelle steht.
     */
    private function firstLineOf(string $output): string
    {
        $first = trim((string) (preg_split('/\R/', trim($output))[0] ?? ''));

        return mb_strlen($first) > 200 ? mb_substr($first, 0, 200).'…' : $first;
    }
}
