<?php

namespace App\Provisioning\Jobs;

use App\Models\Host;
use App\Models\VpnPeer;
use App\Services\Dns\HetznerDnsClient;
use App\Services\Dns\HostDnsDirectory;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;

/**
 * Finalizes host removal off the web request: deletes each run under its runner
 * lock (waiting for any in-flight step to finish rather than racing it), then
 * removes the WireGuard peer and the host record. Runs on the provisioning queue.
 */
class PurgeHost implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    // Retried, because the DNS deletion below can fail for a minute and the job
    // is re-entrant: it re-reads the host, already-deleted runs are simply gone,
    // and the peer removal is idempotent.
    public $tries = 3;

    /** @return list<int> seconds between attempts */
    public function backoff(): array
    {
        return [30, 120];
    }

    public $timeout = 2200;

    public function __construct(public string $uuid)
    {
        $this->onConnection('provisioning');
        $this->onQueue('provisioning');
    }

    public function handle(): void
    {
        $host = Host::query()->where('uuid', $this->uuid)->first();

        if ($host === null) {
            return;
        }

        foreach ($host->runs()->get() as $run) {
            // Block until the runner lock is free (a long step may hold it for up
            // to the step timeout), then delete — cascades events + resources.
            Cache::lock('run:'.$run->uuid, 2200)->block(2100, fn () => $run->delete());
        }

        // Re-read after waiting on the run locks above: a registration that was
        // still running may have written dns_record_id in the meantime, and the
        // stale copy loaded before the wait would skip the deletion and then
        // delete the row that held the id.
        $host->refresh();

        // Legacy: a host onboarded before RegisterHostDns moved off the public
        // Hetzner zone may still carry a public record id here — remove it so
        // a deleted host does not leave its (already-published) management
        // address behind. Hosts onboarded since never set this column; see
        // clupilot:prune-host-dns for the ones already published elsewhere.
        //
        // The record identifier lives on the row about to be deleted, so this
        // has to happen first — otherwise the host's management address stays
        // published with nothing left to clean it up.
        if (filled($host->dns_record_id)) {
            // Deliberately NOT swallowed: the row about to be deleted holds the
            // only reference to that record, so continuing past a failure would
            // leave the machine's management address published for good with no
            // way left to find it. Failing here lets the queue retry, and a
            // permanently broken DNS provider becomes a visible failed job
            // rather than a silent leak.
            app(HetznerDnsClient::class)->deleteRecord($host->dns_record_id);
        }

        // Der interne hostsdir-Eintrag (vpn-dns). Der Name lebt auf der Zeile,
        // die gleich gelöscht wird, also muss er zuerst heraus. `remove()` ist
        // auf einen Eintrag, den es nie gab, ausdrücklich ein Nichtstun — ein
        // Host, der nie bis zur DNS-Registrierung kam, purgt trotzdem sauber.
        if (filled($host->name)) {
            app(HostDnsDirectory::class)->remove($host->name);
        }

        if (filled($host->wg_pubkey)) {
            // Tombstone the console's view of this peer as well. The FK only
            // nulls host_id, which would leave an enabled-but-absent row behind:
            // shown forever as "wird angewendet", and re-enabling it would put a
            // deleted host's key back on the hub. Soft-deleting hands it to the
            // same machinery every other revocation uses — the removal below
            // clears the tombstone, and a sync retries if that removal is lost.
            VpnPeer::query()->where('public_key', $host->wg_pubkey)->delete();

            RemoveWireguardPeer::dispatch($host->wg_pubkey);
        }

        $host->delete();
    }
}
