<?php

namespace App\Actions;

use App\Models\Instance;
use App\Services\Dns\HetznerDnsClient;
use App\Services\Traefik\TraefikWriter;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;

/**
 * Take an instance's address down when its service has genuinely ended.
 *
 * `TraefikWriter::remove()` had no caller anywhere in the application. Every
 * router this platform ever wrote was written for good: the file stayed on the
 * serving host, the hostname kept resolving, and it kept being routed to a guest
 * address that the host is free to hand to somebody else's VM later. That is not
 * a tidiness problem — it is one customer's hostname pointing at another
 * customer's Nextcloud, with a valid certificate on it.
 *
 * ## What "ended" means here
 *
 * Exactly one thing: the instance is `cancellation_scheduled` AND its
 * `service_ends_at` has passed. Not the moment somebody cancels — a customer who
 * schedules a cancellation on the second of the month has paid to the end of the
 * month and must keep working through it, and pulling their address that
 * afternoon would be a serious fault, worse than the leak this closes. Not a
 * `failed` build either: those never got as far as a router, and the ones that
 * did are being retried from the console. A `cancellation_scheduled` row without
 * a `service_ends_at` at all is left alone on purpose — that is a broken record
 * rather than an expired one, and guessing an end date for it would invent the
 * very moment this action exists to respect.
 *
 * ## The DNS record goes with the route
 *
 * Deliberately, and for the same reason the route does. The A record for
 * `{subdomain}.{zone}` is in OUR zone and points at the serving host's public
 * address — a host that goes on serving other customers. Left published it is a
 * name we own, resolving to a live machine, with nothing behind it that belongs
 * to the person it is named after: the shape of every subdomain-takeover there
 * has ever been. It also keeps the customer's old address looking alive, and it
 * would collide the day the same subdomain is issued again. Removing the route
 * without the record would leave the worse half standing.
 *
 * The CUSTOM domain is the opposite case and nothing is done to it. Its A record
 * lives in the customer's own zone, which we have never had access to and never
 * will; it is theirs, it stops reaching us the moment the router goes, and it is
 * for them to point somewhere else.
 *
 * The virtual machine is NOT touched. The founder's cancellation flow promises a
 * finished data export before deprovisioning, and destroying somebody's disks is
 * not a decision this action gets to make on the way past. It takes the address
 * away — which is what was leaking — and leaves the machine for whatever carries
 * out that flow.
 */
class EndInstanceService
{
    public function __construct(
        private TraefikWriter $traefik,
        private HetznerDnsClient $dns,
    ) {}

    /**
     * Has this instance's paid service actually run out?
     *
     * The rule lives here rather than in the command's query so that anything
     * else that ever ends an instance asks the same question — a second copy of
     * "is it over yet" is how one caller ends up a month out from the other.
     */
    public function hasEnded(Instance $instance, ?Carbon $at = null): bool
    {
        return $instance->status === 'cancellation_scheduled'
            && $instance->service_ends_at !== null
            && $instance->service_ends_at->lessThanOrEqualTo($at ?? now());
    }

    /**
     * Tear the address down and mark the instance ended.
     *
     * Re-entrant: removing a router file that is not there is `rm -f`, a record
     * id that has already been deleted is deleted again for nothing, and an
     * instance already marked `ended` is returned false straight away. A DNS
     * failure is deliberately not swallowed — the record id survives on its row,
     * so a retry can finish the job, and a provider that is permanently broken
     * becomes a visible failure rather than a silent leak.
     *
     * @return bool whether this call was the one that ended it
     */
    public function __invoke(Instance $instance): bool
    {
        if (! $this->hasEnded($instance)) {
            return false;
        }

        $host = $instance->host;

        if ($host !== null) {
            // The host that actually serves the traffic — DNS points at it, and
            // it is where ConfigureDnsAndTls wrote the file in the first place.
            $this->traefik->remove($host->wg_ip ?? $host->public_ip, $instance->subdomain);
        } else {
            // No host left to reach: the router file went with the machine it
            // lived on. Said out loud rather than passed over, because it means
            // this teardown is only doing half of what it usually does.
            Log::warning('Ending an instance whose host is gone — no router to remove.', [
                'instance' => $instance->uuid,
            ]);
        }

        foreach ($instance->dnsRecords()->get() as $record) {
            $this->dns->deleteRecord($record->record_id);
            $record->delete();
        }

        $instance->update([
            'status' => 'ended',
            // The address state has to go with the address. Left as it was, a
            // later run would read `route_written` plus a matching hostname list
            // and decide there was nothing to write — so an instance that was
            // ever revived would be announced and routed nowhere.
            'route_written' => false,
            'routed_hostnames' => null,
            'routed_backend' => null,
            'cert_ok' => false,
            'domain_cert_ok' => false,
        ]);

        Log::info('Instance service ended; address withdrawn.', [
            'instance' => $instance->uuid,
            'subdomain' => $instance->subdomain,
            'service_ended_at' => $instance->service_ends_at?->toIso8601String(),
        ]);

        return true;
    }
}
