<?php

namespace App\Actions;

use App\Models\Customer;
use App\Models\Instance;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Provisioning\Jobs\AdvanceRunJob;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

/**
 * Take a customer's machine round once — properly.
 *
 * The product could not restart anything at all. ProxmoxClient had startVm and
 * nothing else, the cloud page's "Neu starten" was a toast, and
 * `restart_required_since` — set by every plan change that touches CPU or memory
 * on a running guest — could only ever be cleared by a resize step that happened
 * to find the machine stopped. Nothing could stop it. So a paid upgrade's cores
 * and RAM were written into the VM's definition and could stay unreached for the
 * life of the contract.
 *
 * This is the missing trigger. It does not do the work: a restart is remote work
 * on a live machine, so it goes through the same run machinery as everything
 * else — see the `restart` pipeline — where it is retried, logged, and visible
 * in the console instead of happening inside a web request.
 */
class RestartInstance
{
    /**
     * May whoever is signed in RIGHT NOW restart this machine?
     *
     * Asked here and not only where the button is drawn. A Livewire action is
     * reachable by anyone who can POST to /livewire/update with a component
     * snapshot — hiding a button hides a button, it does not close a door — so
     * the rule has to be re-checked at the point the run is created.
     *
     * Two identities, two answers (R21): an operator holding `instances.restart`
     * may restart any machine on the estate, because that is what the console is
     * for; a portal user may restart their own and nobody else's. Both guards
     * are read, and either may say yes — the two sessions coexist in one browser
     * by design (see config/auth.php), so asking only the console guard would
     * refuse an operator their own cloud, and asking only the portal guard would
     * refuse every operator everything.
     *
     * The customer is resolved from the signed-in user rather than taken from
     * the caller: a caller that passes the customer is a caller that can pass
     * the wrong one.
     */
    public function allows(Instance $instance): bool
    {
        $operator = Auth::guard('operator')->user();

        if ($operator !== null && $operator->isActive() && $operator->can('instances.restart')) {
            return true;
        }

        $customer = Customer::forUser(Auth::guard('web')->user());

        return $customer !== null && (int) $instance->customer_id === (int) $customer->id;
    }

    /**
     * Start a restart run for this instance, or return null when there is
     * nothing to start.
     *
     * Null is the ordinary "no" and not a failure — a machine that is still
     * being built, one whose VM has gone, one whose service has ended, or one
     * that already has a run in flight all have a better reason to be left
     * alone than a restart has to interrupt them. A refusal on AUTHORISATION is
     * the one case that throws, because it is not an ordinary outcome and the
     * caller must not be able to mistake it for one.
     *
     * @throws AuthorizationException
     */
    public function __invoke(Instance $instance): ?ProvisioningRun
    {
        if (! $this->allows($instance)) {
            throw new AuthorizationException;
        }

        if (! $instance->hasLiveMachine()) {
            return null;
        }

        // The same lock ReapplyInstanceAddress takes, for the same reason: two
        // requests can reach this in one instant — the customer pressing the
        // button while an operator presses it for them is the obvious pair —
        // and two restart runs against one machine would have the second one
        // shutting the guest down while the first waits for it to come back.
        // Nobody waits for the lock: a restart that lost the race has nothing
        // to add, because the run that won does exactly the same thing.
        $lock = Cache::lock('instance-restart:'.$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;

        // Checked against the ORDER, because that is the subject every customer
        // pipeline shares. ANY run, and deliberately not the narrower "does it do
        // this work?" question ReapplyInstanceAddress and ApplyStorageAllowance
        // ask: taking the guest away underneath a run is harmful to every one of
        // them, and unlike a proven domain or a booked pack a restart is somebody
        // pressing a button, so it can be pressed again once the run is done
        // rather than needing to be remembered for them.
        if ($this->hasRunInFlight($order)) {
            return null;
        }

        $run = ProvisioningRun::create([
            'subject_type' => Order::class,
            'subject_id' => $order->id,
            'pipeline' => 'restart',
            'status' => ProvisioningRun::STATUS_PENDING,
            'current_step' => 0,
            // Everything the four steps read: instance_id is how CustomerStep
            // finds the machine, node and vmid are how it reaches 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('Restarting a customer instance.', [
            'instance' => $instance->uuid,
            'restart_pending_since' => $instance->restart_required_since?->toIso8601String(),
            'run' => $run->uuid,
        ]);

        return $run;
    }

    private function hasRunInFlight(Order $order): bool
    {
        return ProvisioningRun::query()
            ->where('subject_type', Order::class)
            ->where('subject_id', $order->id)
            ->inFlight()
            ->exists();
    }
}
