<?php

namespace App\Livewire\Admin;

use App\Models\Datacenter;
use App\Models\Host;
use App\Models\Order;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Validate;
use LivewireUI\Modal\ModalComponent;

/**
 * Edit a datacenter in a modal (avoids the row-height jump of inline editing).
 * Country is picked from a list so it can't be mistyped.
 *
 * The code is editable, but only while nothing depends on it.
 *
 * It was fully immutable, which made a typo permanent. It cannot be freely
 * mutable either, because it is not only a foreign key: hosts.datacenter
 * references it (and MariaDB refuses the update outright, there being no ON
 * UPDATE clause), every past order stores it as plain text, each host's DNS
 * name was minted from it (fsn-01 — a record that exists at the provider and
 * does not move), the machine's own hostname was set from it while it was being
 * built, and the per-code counter that hands out those numbers is keyed by it.
 * Renaming a code with hosts in it is therefore not a rename; it is a datacenter
 * called hel full of machines called fsn-01.
 *
 * So: free while nothing references it — which is the case that matters, a
 * typo noticed shortly after creating one — and locked with the reason named
 * once anything does.
 */
class EditDatacenter extends ModalComponent
{
    public string $uuid = '';

    public string $code = '';

    #[Validate('required|string|max:255')]
    public string $name = '';

    /**
     * Which building, not which city — fsn-dc-15.
     *
     * The provider's own label, free text on purpose: it is theirs to change,
     * and nothing here may key off it. Only so that picking "fsn" twice for
     * redundancy is a decision somebody can actually make.
     */
    #[Validate('nullable|string|max:255')]
    public string $facility = '';

    public string $location = '';

    /** The location present when the modal opened — a pre-curation free-form value. */
    public string $originalLocation = '';

    /**
     * Whether new hosts and orders may still be placed here.
     *
     * The column and the scope existed; the form did not offer it, so a
     * datacenter could be created and never switched off again — the one
     * lifecycle action a location actually needs.
     */
    public bool $active = true;

    /** What references this code, and therefore what stops it being renamed. */
    public int $hostCount = 0;

    public int $orderCount = 0;

    public function mount(string $uuid): void
    {
        $this->authorize('datacenters.manage'); // modals are reachable without the route middleware
        $dc = Datacenter::query()->where('uuid', $uuid)->firstOrFail();
        $this->uuid = $uuid;
        $this->code = $dc->code;
        $this->name = $dc->name;
        $this->facility = (string) $dc->facility;
        $this->location = (string) $dc->location;
        $this->originalLocation = (string) $dc->location;
        $this->active = (bool) $dc->active;
        $this->hostCount = $this->hostsIn($dc->code);
        $this->orderCount = $this->ordersIn($dc->code);
    }

    /**
     * May this code still be changed?
     *
     * Always answered from the database, never from a hydrated property: the
     * lock is an authorisation decision, and a forged request that flipped it
     * would take the DNS names of every host in this datacenter with it.
     */
    public function codeIsFree(string $code): bool
    {
        return $this->hostsIn($code) === 0 && $this->ordersIn($code) === 0;
    }

    private function hostsIn(string $code): int
    {
        return Host::query()->where('datacenter', $code)->count();
    }

    private function ordersIn(string $code): int
    {
        return Order::query()->where('datacenter', $code)->count();
    }

    public function save()
    {
        $this->authorize('datacenters.manage');

        // Accept the configured countries plus the record's own legacy value, so a
        // datacenter created before the curated list can still be edited. The
        // legacy value is read from the DB — never from the client-hydrated
        // property, which could be forged to whitelist an arbitrary location.
        $dc = Datacenter::query()->where('uuid', $this->uuid)->firstOrFail();
        $allowed = array_keys((array) config('countries'));
        if ($dc->location) {
            $allowed[] = $dc->location;
        }
        // Recomputed here, never taken from the property the browser sent back:
        // the lock is what protects every host's DNS name in this datacenter
        // from being orphaned, and a forged request must not be able to lift it.
        $codeIsFree = $this->codeIsFree($dc->code);

        if ($codeIsFree) {
            // Normalised before validating so the unique check matches how the
            // row is stored — otherwise `FSN` passes the rule and the update
            // then collides with an existing lowercased row (mirrors Datacenters).
            $this->code = strtolower(trim($this->code));
        } else {
            // Whatever came back from the browser is discarded. Silently, and
            // deliberately: the field was never offered, so there is nothing to
            // report to an honest client, and a dishonest one gets no purchase.
            $this->code = $dc->code;
        }

        $rules = [
            'name' => 'required|string|max:255',
            'facility' => 'nullable|string|max:255',
            'location' => ['nullable', Rule::in($allowed)], // array form — a legacy value may contain commas
            'active' => 'boolean',
        ];

        if ($codeIsFree) {
            // The same rule the create form uses: a code becomes a DNS label
            // (fsn-01.node.clupilot.com), so it has to be one. Ignoring its own
            // row, or saving without touching the code would collide with itself.
            $rules['code'] = [
                'required', 'string', 'max:8',
                'regex:/^[a-z0-9]([a-z0-9-]{0,6}[a-z0-9])?$/',
                Rule::unique('datacenters', 'code')->ignore($dc->id),
            ];
        }

        $data = $this->validate($rules);

        // Only on the transition, and compared against what is STORED: an
        // already-inactive location would otherwise announce that it had just
        // been switched off every time its name was edited.
        $justDeactivated = $dc->active && ! $data['active'];
        $hostsLeftRunning = $justDeactivated ? $dc->hosts()->count() : 0;

        Datacenter::query()->where('uuid', $this->uuid)->update([
            // Only where it was actually offered. $data has no 'code' key at
            // all when the field was locked, and writing $this->code back would
            // be the same value anyway — but only by luck, and this is the line
            // that would quietly stop being true if the lock ever moved.
            ...($codeIsFree ? ['code' => $data['code']] : []),
            'name' => $data['name'],
            'facility' => $data['facility'] ?: null,
            'location' => $data['location'] ?: null,
            'active' => $data['active'],
        ]);

        // One message, not two. Both go through the same toast, and the second
        // replaces the first — so the generic "saved" would swallow the only
        // sentence that says the hosts there are still running.
        $this->dispatch('notify', message: $hostsLeftRunning > 0
            ? __('datacenters.deactivated_with_hosts', ['n' => $hostsLeftRunning])
            : __('datacenters.updated'));

        return $this->redirectRoute('admin.datacenters', navigate: true);
    }

    public function render()
    {
        // Read again rather than shown from the mounted properties: a host can
        // be onboarded into this datacenter while the modal sits open, and an
        // input offered on the strength of a stale count would be refused on
        // save with nothing on screen explaining why.
        $stored = Datacenter::query()->where('uuid', $this->uuid)->value('code') ?? $this->code;
        $this->hostCount = $this->hostsIn($stored);
        $this->orderCount = $this->ordersIn($stored);

        return view('livewire.admin.edit-datacenter', [
            'countries' => config('countries'),
            'codeFree' => $this->hostCount === 0 && $this->orderCount === 0,
        ]);
    }
}
