<?php

namespace App\Actions;

use App\Models\Instance;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Provisioning\Jobs\AdvanceRunJob;
use App\Provisioning\WorkInFlight;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

/**
 * Make an instance's address true again — the router, the certificate and the
 * hostname Nextcloud answers to — without touching anything else about it.
 *
 * A custom domain used to be announced and never served: the portal, the
 * credentials mail and Instance::address() all reported it the moment its TXT
 * proof appeared, while the only thing that ever wrote a route was the initial
 * provisioning run, and it wrote the platform subdomain. This is the missing
 * half — the moment a proven domain becomes an address, and the moment a
 * withdrawn one stops being one.
 *
 * Called from the three places the address can change: the nightly proof check
 * when verification flips, the customer's own domain page, and a package change
 * that takes the right to a domain away. Deliberately not called from anywhere
 * that merely LOOKS at the domain — a re-apply is remote work on a live
 * machine, so it happens on a change and not on a schedule.
 */
class ReapplyInstanceAddress
{
    /**
     * Start an address run for this instance, or return null when there is
     * nothing to start.
     *
     * Null is the ordinary answer, not a failure: an instance still being
     * built, one whose machine no longer exists, or one that already has a run
     * in flight all have their address applied by that run instead.
     */
    public function __invoke(?Instance $instance): ?ProvisioningRun
    {
        if ($instance === null || ! $this->isReapplyable($instance)) {
            return null;
        }

        // The lock closes the window between asking whether a run exists and
        // creating one. Two requests can reach this in the same instant — the
        // customer's own "check now" and the nightly command are the obvious
        // pair — and two runs against one machine would write the same router
        // twice and race each other's occ calls. Nobody waits for the lock: a
        // re-apply that lost the race has nothing to add, because the run that
        // won reads the same domain state it would have.
        $lock = Cache::lock('instance-address:'.$instance->uuid, 30);

        if (! $lock->get()) {
            return null;
        }

        try {
            return $this->start($instance);
        } finally {
            $lock->release();
        }
    }

    private function start(Instance $instance): ?ProvisioningRun
    {
        $order = $instance->order;

        // A run already under way applies whatever the domain state says when it
        // gets to the step — but only if it HAS the step. Asked of the two steps
        // this pipeline consists of rather than of the mere existence of a run:
        // `customer` and `plan-change` both carry them and there is nothing to
        // add, while `restart`, `storage` and `quota` carry neither, and standing
        // aside for one of those was how a domain proven during a restart stayed
        // unrouted for good. See App\Provisioning\WorkInFlight.
        if (app(WorkInFlight::class)->covers($order, 'address')) {
            return null;
        }

        $run = ProvisioningRun::create([
            'subject_type' => Order::class,
            'subject_id' => $order->id,
            'pipeline' => 'address',
            'status' => ProvisioningRun::STATUS_PENDING,
            'current_step' => 0,
            // Everything the two steps read. instance_id is how CustomerStep
            // finds the machine; node and vmid are how it reaches inside it.
            'context' => [
                'instance_id' => $instance->id,
                'host_id' => $instance->host_id,
                'node' => $instance->host?->node,
                'vmid' => $instance->vmid,
                'subdomain' => $instance->subdomain,
            ],
        ]);

        AdvanceRunJob::dispatch($run->uuid);

        Log::info('Re-applying an instance address.', [
            'instance' => $instance->uuid,
            'domain' => $instance->custom_domain,
            'verified' => $instance->domainIsVerified(),
            'run' => $run->uuid,
        ]);

        return $run;
    }

    /**
     * Is there a machine here whose address can be re-applied at all?
     *
     * The question is not this action's alone — a plan change asks exactly the
     * same one before it starts a run against a live machine — so the rule sits
     * on the model and both read it there. See Instance::hasLiveMachine().
     */
    private function isReapplyable(Instance $instance): bool
    {
        return $instance->hasLiveMachine();
    }
}
