<?php

namespace App\Provisioning\Steps\Host;

use App\Models\Host;
use App\Models\ProvisioningRun;
use App\Models\RunResource;
use App\Provisioning\Contracts\ProvisioningStep;
use App\Provisioning\Steps\ManagesRunResources;
use App\Services\Secrets\SecretVault;
use App\Services\Ssh\RemoteShell;

/**
 * Shared helpers for host-onboarding steps: subject resolution, key login, and
 * idempotency breadcrumbs (run_resources).
 */
abstract class HostStep implements ProvisioningStep
{
    use ManagesRunResources;

    public function label(): string
    {
        return 'hosts.step.'.$this->key();
    }

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

    protected function host(ProvisioningRun $run): Host
    {
        /** @var Host $host */
        $host = $run->subject;

        return $host;
    }

    /**
     * Reach the host over the WireGuard tunnel once the tunnel is PROVEN, and
     * over its public IP until then.
     *
     * The distinction is the whole point. `wg_ip` is written inside the
     * allocation lock in ConfigureWireguard, before the handshake has been
     * verified even once — so "wg_ip is filled" means "an address has been
     * reserved", not "SSH works over the tunnel". Dialling the tunnel on the
     * strength of the reservation alone produced an unrecoverable dead end:
     * attempt 1 reserved the address and then failed at the handshake, and every
     * later attempt — including an operator pressing Retry in the console — died
     * connecting to a tunnel that did not work, before reaching the only code
     * that could have repaired wg0.conf. Recovery meant nulling hosts.wg_ip by
     * hand. The same shape sat under RebootIntoPveKernel, ConfigureProxmox,
     * CreateAutomationToken and SecureHostFirewall, which all call this method.
     *
     * tunnelProven() reads the `wg_peer` breadcrumb, which ConfigureWireguard
     * records ONLY after a successful handshake — the one fact in the database
     * that means "this tunnel has carried traffic".
     *
     * Falling back to the public IP is not a way around the firewall: port 22 on
     * the public IP is only open until SecureHostFirewall runs, that step runs
     * last, and by the time it has run the tunnel is necessarily proven (it is
     * the same `wg_peer` breadcrumb, recorded several steps earlier, and
     * SecureHostFirewall additionally re-checks the handshake from the host's own
     * side before writing a single rule). So the fallback only ever applies while
     * 22 is still open anyway, and stops applying exactly when it closes.
     */
    protected function keyLogin(RemoteShell $shell, Host $host): void
    {
        // The identity in force: the one stored in the console if there is
        // one, else CLUPILOT_SSH_PRIVATE_KEY(_PATH) — read here, at the
        // point of use, per SecretVault's docblock (rule 3).
        $shell->connectWithKey(
            $this->tunnelProven($host) ? $host->wg_ip : $host->public_ip,
            'root',
            (string) app(SecretVault::class)->get('ssh.private_key'),
            $host->ssh_host_key, // pinned during EstablishSshTrust
        );
    }

    /**
     * Has a WireGuard handshake to this host ever succeeded?
     *
     * Scoped to the HOST, not to one run: the tunnel is a property of the
     * machine, and every maintenance run made after onboarding must keep using
     * it — a run-scoped lookup would send a later run to a public IP where 22 is
     * closed. Nothing prunes provisioning runs or their resources, so the
     * breadcrumb outlives the run that wrote it.
     */
    protected function tunnelProven(Host $host): bool
    {
        if (blank($host->wg_ip)) {
            return false;
        }

        return RunResource::query()
            ->where('host_id', $host->id)
            ->where('kind', 'wg_peer')
            ->exists();
    }
}
