<?php

namespace App\Livewire\Admin;

use App\Models\Customer;
use App\Models\Host;
use App\Models\ProvisioningRun;
use App\Models\SecurityBlock;
use App\Provisioning\Jobs\AdvanceRunJob;
use App\Provisioning\Jobs\CollectHostLoad;
use App\Services\Proxmox\HostLoadSeries;
use App\Support\HostName;
use App\Support\PveVersion;
use Illuminate\Support\Collection;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;

#[Layout('layouts.admin')]
class HostDetail extends Component
{
    public Host $host;

    public function mount(Host $host): void
    {
        $this->host = $host;

        // Nothing collected yet? Ask for it now rather than leaving the tiles
        // empty until the minutely collector next comes round. Opening the page
        // is exactly the moment somebody wants these figures, and waiting up to
        // a minute for the first ones reads as "broken", not as "not yet".
        //
        // The job is ShouldBeUnique, so a page opened twice does not queue two.
        $worthAsking = Host::query()->collectable()->whereKey($host->id)->exists();

        if ($worthAsking && ! app(HostLoadSeries::class)->forHost($host)['available']) {
            CollectHostLoad::dispatch();
        }
    }

    /** Live refresh whenever any run advances (admins-only channel). */
    #[On('echo-private:admin.runs,StepAdvanced')]
    public function onStepAdvanced(): void
    {
        $this->host->refresh();
    }

    /** Adjust the capacity reserve (% of storage kept free for headroom). */
    public function saveReserve(int $reserve): void
    {
        $this->authorize('hosts.manage');
        $reserve = max(0, min(90, $reserve));
        $this->host->update(['reserve_pct' => $reserve]);
        $this->dispatch('notify', message: __('hosts.detail.reserve_saved'));
    }

    /**
     * Drain / return a host: toggle between active and disabled. Disabled takes
     * it out of placement (maintenance) without purging it — distinct from the
     * destructive "remove host". Never touches a host mid-onboarding.
     */
    public function toggleMaintenance(): void
    {
        $this->authorize('hosts.manage');
        if ($this->host->status === 'active') {
            $this->host->update(['status' => 'disabled']);
        } elseif ($this->host->status === 'disabled') {
            $this->host->update(['status' => 'active']);
        }
    }

    /**
     * "Eigener Server" verkauft, ab jetzt auch Tatsache: der Host gehört
     * diesem Kunden, und Host::placeableIn() hält ab sofort alle anderen
     * fern. Braucht keine Bestätigung — niemandem wird etwas weggenommen.
     */
    public function reserveFor(string $customerUuid): void
    {
        $this->authorize('hosts.manage');

        $customer = Customer::query()->where('uuid', $customerUuid)->first();

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

        $this->host->update(['reserved_for_customer_id' => $customer->id]);
        $this->dispatch('notify', message: __('hosts.detail.reserved_saved', ['name' => $customer->name]));
    }

    /**
     * Gibt die Maschine an den allgemeinen Bestand zurück.
     *
     * Nicht automatisch beim Kundenaustritt (siehe Migrationskommentar): sonst
     * verschwände eine Maschine kommentarlos aus dem Bestand, ohne dass ein
     * Operator sie tatsächlich freigegeben hat.
     */
    public function releaseReservation(): void
    {
        $this->authorize('hosts.manage');
        $this->host->update(['reserved_for_customer_id' => null]);
        $this->dispatch('notify', message: __('hosts.detail.reserved_released'));
    }

    /** ConfirmReleaseReservation dispatches this back (R23) — see that class. */
    #[On('host-reservation-release-confirmed')]
    public function onReleaseConfirmed(): void
    {
        $this->releaseReservation();
    }

    /**
     * ConfirmReleaseBlock (Konsole) dispatches this back (R23) — see that
     * class. Es mutiert nichts; die eigentliche Freigabe UND die Pruefung,
     * dass die Sperre wirklich an DIESEM Host haengt, stehen hier — ein
     * Modal ist ohne die Route-Middleware dieser Seite erreichbar (R20).
     */
    #[On('security-block-release-confirmed')]
    public function onSecurityBlockReleaseConfirmed(string $uuid): void
    {
        $this->authorize('hosts.manage');

        $block = SecurityBlock::query()->where('uuid', $uuid)->where('host_id', $this->host->id)->first();

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

        // Siehe SecurityBlock::release() — der Rueckgabewert sagt, ob die
        // Firewall schon nachgezogen hat, nicht ob der Datensatz steht.
        $this->dispatch('notify', message: $block->release(auth('operator')->user())
            ? __('admin.security_block.released')
            : __('admin.security_block.released_pending'));
    }

    public function retry(): void
    {
        $this->authorize('hosts.manage');
        $run = $this->currentRun();

        if ($run !== null && $run->status === ProvisioningRun::STATUS_FAILED) {
            $run->update([
                'status' => ProvisioningRun::STATUS_RUNNING,
                'attempt' => 0,
                'next_attempt_at' => now(),
                'started_at' => now(), // reset the step timer so it doesn't re-time-out instantly
                'error' => null,
            ]);
            $this->host->update(['status' => 'onboarding']);
            AdvanceRunJob::dispatch($run->uuid);
        }
    }

    private function currentRun(): ?ProvisioningRun
    {
        return $this->host->runs()->latest('id')->first();
    }

    /** @return array<int, array{label: string, state: string}> */
    private function buildSteps(?ProvisioningRun $run): array
    {
        $pipeline = config('provisioning.pipelines.host', []);
        $current = $run?->current_step ?? 0;
        $status = $run?->status;

        $steps = [];
        foreach ($pipeline as $index => $class) {
            if ($status === ProvisioningRun::STATUS_COMPLETED || $index < $current) {
                $state = 'done';
            } elseif ($index === $current) {
                $state = $status === ProvisioningRun::STATUS_FAILED ? 'failed' : 'running';
            } else {
                $state = 'pending';
            }

            $steps[] = ['label' => __(app($class)->label()), 'state' => $state];
        }

        return $steps;
    }

    /**
     * The most recent reading that is actually a reading.
     *
     * Walked from the back rather than taking the last element: Proxmox omits
     * the fields for a sample it has no data for, so the newest entry is quite
     * often a gap — and a tile that showed 0 % because of it would be stating
     * something about the host that nobody measured.
     *
     * @param  array<int, ?float>  $series
     */
    private function latest(array $series): ?float
    {
        for ($i = count($series) - 1; $i >= 0; $i--) {
            if ($series[$i] !== null) {
                return $series[$i];
            }
        }

        return null;
    }

    public function render()
    {
        $run = $this->currentRun();

        /** @var Collection $events */
        $events = $run
            ? $run->events()->latest('id')->limit(30)->get()
            : collect();

        $load = app(HostLoadSeries::class)->forHost($this->host);

        return view('livewire.admin.host-detail', [
            'run' => $run,
            'steps' => $this->buildSteps($run),
            'events' => $events,
            'instances' => $this->host->instances()->latest('id')->get(),
            'health' => $this->host->healthState(),
            'load' => $load,
            // Eine Zahl je Kachel, aus derselben Reihe, die die Kachel zeichnet.
            'now' => [
                'cpu' => $this->latest($load['cpu']),
                'ram' => $this->latest($load['ram']),
                'netin' => $this->latest($load['netin']),
                'netout' => $this->latest($load['netout']),
            ],
            'version' => PveVersion::parse($this->host->pve_version),
            'fqdn' => HostName::fqdn($this->host->name),
            'securityBlocks' => SecurityBlock::query()
                ->where('host_id', $this->host->id)
                ->orderByDesc('blocked_at')
                ->get(),
            // Nur gebraucht, solange der Host noch niemandem gehört — aber
            // billig genug (eine Namensliste), um sie immer mitzugeben statt
            // eine zweite Bedingung ums Laden zu ziehen.
            'customers' => Customer::query()->orderBy('name')->get(),
        ]);
    }
}
