<?php

namespace App\Livewire\Admin;

use App\Actions\RestartInstance;
use App\Models\Instance;
use App\Services\Billing\StorageAllowance;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Lang;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;
use Livewire\WithPagination;

/**
 * Every instance on the estate, from the instances table.
 *
 * This page used to list seven invented instances on four invented hosts,
 * complete with a Nextcloud version column. The version is not recorded
 * anywhere, so the column is gone rather than filled in with something
 * plausible — and the storage column now shows the quota that was actually
 * sold, not a made-up "used of total", because used disk is not collected.
 */
#[Layout('layouts.admin')]
class Instances extends Component
{
    use WithPagination;

    /**
     * Restart one instance, once ConfirmRestartInstance has said so.
     *
     * The capability is checked twice on purpose, and neither check is the
     * decoration of the other. This one keeps an operator without
     * `instances.restart` from driving the console component at all; the one
     * inside RestartInstance is what actually guards the machine, because that
     * is the door a hand-written POST to /livewire/update arrives at. Losing
     * either would still leave a customer's cloud reachable by somebody who was
     * only ever meant to read the list.
     */
    #[On('instance-restart-confirmed')]
    public function restart(string $uuid): void
    {
        $this->authorize('instances.restart');

        $instance = Instance::query()->where('uuid', $uuid)->first();

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

        try {
            $run = app(RestartInstance::class)($instance);
        } catch (AuthorizationException) {
            $this->dispatch('notify', message: __('admin.restart_denied'));

            return;
        }

        // Null is every ordinary refusal — no live machine, or a run already in
        // flight against this order. Said out loud, because an operator who
        // pressed a button and saw nothing will press it again.
        $this->dispatch('notify', message: __($run === null ? 'admin.restart_busy' : 'admin.restart_started'));
    }

    public function render()
    {
        $instances = Instance::query()
            ->with(['customer', 'host'])
            ->orderByDesc('id')
            ->paginate(25);

        return view('livewire.admin.instances', [
            'instances' => $instances,
            // Die bestellten Datenexporte, ueber der Liste.
            //
            // `export_wish` stand bis hierher in einer Spalte, die kein Mensch
            // zu Gesicht bekam: ein „Ja" aus dem Kuendigungsdialog hatte in
            // der ganzen Konsole keinen einzigen Leser, und selbst ein Ausweg
            // von Hand haette eine Datenbankabfrage gebraucht. Eine Frage
            // ohne Empfaenger ist keine Frage.
            //
            // Bewusst ein eigener Abschnitt und keine Plakette in der Zeile:
            // die Tabelle darunter ist nach id absteigend seitenweise
            // geblaettert, ein vor Monaten bestellter Export saesse also auf
            // Seite acht. Und bewusst hier statt auf der Uebersicht — die
            // Liste ist die Seite, auf die ein Betreiber nach einer
            // Kuendigung ohnehin schaut, und ein Hinweis auf der Startseite,
            // den nichts je wieder abraeumen kann, waere Moebel.
            //
            // Nach Laufzeitende sortiert, nicht nach id: der naechste Termin
            // steht oben. `ended` bleibt drin — dann ist der Export faellig,
            // nicht erledigt.
            'exports' => Instance::query()
                ->with('customer')
                ->where('export_wish', true)
                ->whereIn('status', ['cancellation_scheduled', 'ended'])
                ->orderBy('service_ends_at')
                ->get()
                ->map(fn (Instance $i) => [
                    'customer' => $i->customer?->name ?? '—',
                    'address' => $i->domainIsVerified() ? $i->custom_domain : $i->subdomain,
                    // R19: die Wanduhr des Betreibers, nicht die Speicherzone.
                    'ends_at' => $i->service_ends_at?->local()->isoFormat('LL') ?? '—',
                    'over' => $i->service_ends_at !== null && $i->service_ends_at->isPast(),
                ])
                ->all(),
            // Whether to draw the action column at all. An operator who may not
            // restart anything should not be reading a column of buttons that
            // answer them 403.
            'canRestart' => Gate::allows('instances.restart'),
            'rows' => $instances->getCollection()->map(fn (Instance $i) => [
                // R11: a row's action is addressed by uuid, never the numeric id.
                'uuid' => $i->uuid,
                // Only a machine remote work can actually reach. A reservation
                // with no VM, a failed build and an ended service all have
                // nothing to restart — see Instance::hasLiveMachine().
                'live' => $i->hasLiveMachine(),
                // The operator list shows what is actually served — an unverified
                // custom domain is a plan, not an address.
                'address' => $i->domainIsVerified() ? $i->custom_domain : $i->subdomain,
                'customer' => $i->customer?->name ?? '—',
                'host' => $i->host?->name ?? '—',
                'vmid' => $i->vmid ?? '—',
                'plan' => $i->plan !== null ? __('billing.plan.'.$i->plan) : '—',
                // The whole allowance, packs included — the same figure the
                // customer's own page states and the same one Nextcloud is
                // told. An operator reading the package alone here would be
                // looking at a smaller number than the machine enforces and
                // would have no way of telling.
                'quota' => ($owed = StorageAllowance::for($i)->totalGb()) > 0 ? $owed.' GB' : '—',
                // A machine running on less CPU or RAM than it has been sold —
                // see Instance::restartIsPending(). Shown beside the status
                // rather than as one, because the instance is genuinely active
                // and the operator needs both facts at once.
                'restart' => $i->restartIsPending(),
                'status' => $status = $i->status ?? 'provisioning',
                // A status the lifecycle adds later must show as itself, never
                // as "admin.status.whatever" in front of the owner.
                'status_label' => Lang::has('admin.status.'.$status)
                    ? __('admin.status.'.$status)
                    : $status,
            ])->all(),
        ]);
    }
}
