<?php

namespace App\Provisioning\Steps\Host;

use App\Models\Host;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Ssh\RemoteShell;
use App\Services\Wireguard\WireguardHub;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

/**
 * Installs WireGuard on the host, allocates a management IP, registers the host
 * as a peer on the CluPilot hub, and verifies the tunnel is up.
 */
class ConfigureWireguard extends HostStep
{
    public function __construct(private RemoteShell $shell, private WireguardHub $hub) {}

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

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

        // Idempotent replay: tunnel already fully provisioned — just re-verify it.
        if (filled($host->wg_ip) && $this->hasResource($run, 'wg_peer')) {
            return $this->verifyHandshake();
        }

        if (! $this->shell->run('export DEBIAN_FRONTEND=noninteractive; apt-get install -y wireguard')->ok()) {
            return StepResult::retry(30, 'installing wireguard failed');
        }
        $this->shell->run('test -f /etc/wireguard/privatekey || (umask 077; wg genkey > /etc/wireguard/privatekey)');

        $publicKey = trim($this->shell->run('wg pubkey < /etc/wireguard/privatekey')->stdout);
        if (blank($publicKey)) {
            return StepResult::retry(20, 'could not read host WireGuard public key');
        }

        // Allocate + reserve the management IP atomically so concurrent onboarding
        // runs can never receive the same address.
        $wgIp = Cache::lock('wireguard:allocate', 30)->block(10, function () use ($host) {
            $ip = $host->wg_ip ?: $this->hub->allocateIp();
            if (blank($host->wg_ip)) {
                $host->update(['wg_ip' => $ip]);
            }

            return $ip;
        });

        $privateKey = trim($this->shell->run('cat /etc/wireguard/privatekey')->stdout);
        $this->shell->putFile('/etc/wireguard/wg0.conf', $this->renderConfig($wgIp, $privateKey));

        if (! $this->shell->run('systemctl enable --now wg-quick@wg0 || wg-quick up wg0')->ok()) {
            return StepResult::retry(20, 'bringing up wg0 failed');
        }

        // See RemoveWireguardPeer: hub mutations are serialized against the
        // reconciliation that feeds the VPN console.
        Cache::lock('wireguard:hub', 30)->block(10, fn () => $this->hub->addPeer($publicKey, $wgIp));

        // Store the pubkey now (so removal can always clean up the hub peer), but
        // record the wg_peer resource only AFTER the handshake verifies — the
        // idempotent short-circuit must not skip an un-verified configuration.
        $host->update(['wg_pubkey' => $publicKey]);

        if ($this->verifyHandshake()->type !== StepResult::ADVANCE) {
            return StepResult::retry(15, 'WireGuard handshake not up yet');
        }

        $this->recordResource($run, $host, 'wg_peer', $publicKey);

        return StepResult::advance();
    }

    private function verifyHandshake(): StepResult
    {
        $hubIp = (string) config('provisioning.wireguard.hub_ip');

        return $this->shell->run('ping -c1 -W2 '.escapeshellarg($hubIp))->ok()
            ? StepResult::advance()
            : StepResult::retry(15, 'WireGuard handshake not up yet');
    }

    private function renderConfig(string $wgIp, string $privateKey): string
    {
        $subnet = (string) config('provisioning.wireguard.subnet', '10.66.0.0/24');
        $prefix = Str::afterLast($subnet, '/'); // honour the configured prefix length

        return implode("\n", [
            '[Interface]',
            "Address = {$wgIp}/{$prefix}",
            "PrivateKey = {$privateKey}",
            '',
            '[Peer]',
            'PublicKey = '.$this->hub->publicKey(),
            'Endpoint = '.$this->hub->endpoint(),
            "AllowedIPs = {$subnet}",
            'PersistentKeepalive = 25',
            '',
        ]);
    }
}
