<?php

namespace App\Livewire\Admin;

use App\Models\Host;
use App\Services\Provisioning\HostCapacity;
use App\Services\Provisioning\ServerMarket;
use Livewire\Attributes\Layout;
use Livewire\Component;

/**
 * Who is waiting for a machine, and what machine would clear them.
 *
 * A paid order that cannot be placed is parked rather than failed — the
 * customer has paid, and "no host has room" is a purchasing decision, not a
 * fault. This is where that decision is taken: what is queued, what the
 * roomiest host still has, what the queue needs, and what such a machine costs
 * today.
 *
 * Nothing here buys anything. Ordering a server is a contract; a bug in a
 * capacity calculation must not be able to sign one.
 */
#[Layout('layouts.admin')]
class Capacity extends Component
{
    /**
     * Look again now.
     *
     * The runner retries every couple of minutes on its own. This is for the
     * operator who has just finished onboarding a host and does not want to
     * wait for the tick to see the queue drain.
     */
    public function recheck(): void
    {
        $this->authorize('hosts.manage');

        // Through the same query that FINDS them, so "look again" can never
        // mean a different set of runs than the table above it is showing.
        app(HostCapacity::class)->parkedRuns()->update(['next_attempt_at' => now()]);

        $this->dispatch('notify', message: __('capacity.rechecked'));
    }

    /**
     * Decide which parked order goes on which host.
     *
     * The queue is read by somebody who knows things the placement rule does
     * not — that this customer belongs beside that one, that a machine is about
     * to be taken out of service, that the big order should have the new box
     * and the small ones can share. The rule stays in charge when nobody says
     * otherwise; an empty choice hands it back.
     *
     * Only the preference is written. The reservation still happens in the
     * provisioning step, under the placement lock, and still checks the host
     * has room — a console that created the instance itself would be a second
     * placement path with no lock and its own arithmetic.
     */
    public function pin(int $runId, string $hostId): void
    {
        $this->authorize('hosts.manage');

        // Only a run that is actually parked. Writing a host preference onto a
        // run that has moved on would sit in its context for ever and take
        // effect on a re-run months later, on a host chosen for a queue that no
        // longer exists.
        $run = app(HostCapacity::class)->parkedRuns()->whereKey($runId)->first();

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

        if ($hostId === '') {
            $run->forgetContext('preferred_host_id');
            $this->dispatch('notify', message: __('capacity.pin_cleared'));

            return;
        }

        // Nur ein Host, auf dem diese Bestellung auch landen dürfte:
        // unreserviert oder ihrem eigenen Kunden reserviert, über denselben
        // Scope, den die Platzierung im Schritt benutzt. Eine reservierte
        // Maschine gehört ihrem Kunden — sie hier anheften zu lassen hieße, die
        // Zusage über die Konsole wieder aufzumachen, und der Schritt lehnte
        // die Bestellung danach ohnehin ab.
        $host = Host::query()
            ->where('status', 'active')
            ->unreserved($run->subject?->customer_id)
            ->find((int) $hostId);

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

        $run->mergeContext(['preferred_host_id' => $host->id]);
        // Taken up now rather than at the next tick: an operator who has just
        // said where it goes is watching for it to go there.
        $run->forceFill(['next_attempt_at' => now()])->save();

        $this->dispatch('notify', message: __('capacity.pinned', ['host' => $host->name]));
    }

    public function render()
    {
        $this->authorize('hosts.manage');

        $capacity = app(HostCapacity::class);
        $demand = $capacity->queueDemand();
        $parked = $capacity->parked();

        // Die Auswahl je Bestellung, nicht eine Liste für alle: welcher Host in
        // Frage kommt, hängt am Kunden dieser Bestellung. Gefragt wird über
        // denselben Scope wie in der Platzierung — im eigenen Rechenzentrum
        // (der Schritt lehnt einen Pin von anderswo ab) und nicht jemand
        // anderem reserviert. Ein Auswahlfeld, das anbietet, was die Aktion
        // danach ablehnt, ist eine Falle.
        //
        // Eine Abfrage je Rechenzentrum/Kunde-Paar statt je Zeile: die
        // Warteschlange ist die Liste bezahlter Bestellungen ohne Maschine, also
        // kurz, und mehrere davon gehören meist demselben Rechenzentrum.
        $choices = [];

        foreach ($parked as $index => $entry) {
            $customerId = $entry['order']->customer_id;
            $key = $entry['datacenter'].'|'.$customerId;

            $choices[$key] ??= Host::query()
                ->where('status', 'active')
                ->where('datacenter', $entry['datacenter'])
                ->unreserved($customerId)
                ->orderBy('name')
                ->get();

            $parked[$index]['choices'] = $choices[$key];
        }

        return view('livewire.admin.capacity', [
            'parked' => $parked,
            'demand' => $demand,
            'unplaceable' => $capacity->unplaceablePlans(),
            // reservedFor eager-loaded: die Tafel "was die Hosts noch
            // aufnehmen" weist eine reservierte Maschine als solche aus,
            // statt sie kommentarlos neben dem allgemeinen Bestand zu zeigen.
            'hosts' => Host::query()->where('status', 'active')->orderBy('name')->with('reservedFor')->get(),
            // Only when something is actually waiting. A shopping list on an
            // idle estate is an invitation to spend money for no reason.
            'offers' => $demand['count'] > 0
                ? app(ServerMarket::class)->offers($demand['largest'])
                : [],
        ]);
    }
}
