<?php

namespace App\Livewire\Admin;

use App\Models\Operator;
use App\Models\ProxyHost;
use App\Services\Deployment\UpdateChannel;
use App\Services\Proxy\CertificateSweep;
use App\Support\Settings;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

/**
 * Die Hostnamen dieser Installation — anlegen, ansehen, anwenden.
 *
 * Entstanden aus einem Handgriff, der teuer war: `files.clupilot.com` hatte
 * einen DNS-Eintrag, hatte `FILES_HOST` in der Umgebung, lag auf der richtigen
 * Maschine — und hatte trotzdem kein Zertifikat, weil `/etc/caddy/Caddyfile`
 * von Hand gepflegt wird und niemand daran dachte, dass das ein zweiter,
 * getrennter Schritt ist. Nichts in der Konsole hätte das verraten.
 *
 * Zwei Dinge deshalb an einer Stelle:
 *
 *  - Der WUNSCH: welche Namen bedient werden sollen. Er geht über den Agenten
 *    in die Proxy-Konfiguration, ohne dass sich jemand auf den Server meldet.
 *  - Die WIRKLICHKEIT: ob der Name ein Zertifikat hat und wie lange noch.
 *    Gemessen an dem, was ein Besucher bekommt — nicht aus der Konfiguration
 *    gelesen, denn genau die sah im Fehlerfall richtig aus.
 */
#[Layout('layouts.admin')]
class ProxyHosts extends Component
{
    #[Validate]
    public string $hostname = '';

    public string $purpose = 'public';

    public string $note = '';

    public string $acmeEmail = '';

    public ?string $notice = null;

    public function mount(CertificateSweep $sweep): void
    {
        $this->authorize('site.manage');
        $this->acmeEmail = (string) Settings::get('proxy.acme_email', '');

        // Die Namen aus der Umgebung übernehmen, damit die Seite NIE leer ist.
        //
        // Die erste Fassung war ein leeres Register: sie zeigte nichts, während
        // die Installation längst ein halbes Dutzend Namen bediente, deren
        // Zertifikate der Betreiber gerade sehen wollte. Eine Übersicht, die man
        // erst befüllen muss, beantwortet „was habe ich" nicht.
        //
        // Nur der Abgleich, NICHT die Messung: die geht ins Netz, und ein
        // halbes Dutzend TLS-Handschläge beim Öffnen einer Seite bedeutet
        // Warten — mit einem Namen darunter, der gerade nicht auflöst.
        $sweep->sync();
    }

    protected function rules(): array
    {
        return [
            // Ein Hostname und nichts anderes. Der root-eigene Helfer prüft es
            // ein zweites Mal — das hier ist die Höflichkeit, jenes die Sperre.
            'hostname' => [
                'required', 'string', 'max:253',
                'regex:/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/',
                Rule::unique('proxy_hosts', 'hostname'),
            ],
            'purpose' => ['required', Rule::in(ProxyHost::PURPOSES)],
            'note' => ['nullable', 'string', 'max:255'],
        ];
    }

    public function add(): void
    {
        $this->authorize('site.manage');
        $data = $this->validate();

        ProxyHost::create($data);

        $this->reset('hostname', 'note');
        $this->purpose = 'public';
        $this->notice = __('proxy.added');
    }

    public function remove(int $id): void
    {
        $this->authorize('site.manage');

        $host = ProxyHost::query()->whereKey($id)->first();

        // Ein Name aus der Umgebung kommt beim nächsten Abgleich wieder. Ihn
        // löschen zu lassen wäre ein Knopf, der nichts bewirkt — schlimmer als
        // keiner, weil er einmal geglaubt wird.
        if ($host === null || $host->isFromConfig()) {
            $this->notice = __('proxy.remove_from_config');

            return;
        }

        $host->delete();

        // Kein Anwenden von selbst. Einen Namen aus der Liste zu nehmen und ihn
        // im selben Atemzug aus dem laufenden Proxy zu entfernen wäre zwei
        // Entscheidungen in einem Klick — und die zweite nimmt eine Seite vom
        // Netz.
        $this->notice = __('proxy.removed');
    }

    public function saveAcmeEmail(): void
    {
        $this->authorize('site.manage');
        $this->validateOnly('acmeEmail', ['acmeEmail' => ['nullable', 'email', 'max:255']]);

        Settings::set('proxy.acme_email', trim($this->acmeEmail));
        $this->notice = __('proxy.email_saved');
    }

    /**
     * Was ein Besucher gerade wirklich bekommt.
     *
     * Nacheinander und mit kurzem Zeitablauf: die Liste ist zweistellig, und
     * ein Hostname, der nicht auflöst, darf die Seite nicht minutenlang
     * blockieren.
     */
    public function checkCertificates(CertificateSweep $sweep): void
    {
        $this->authorize('site.manage');

        $sweep->sync();
        $result = $sweep->check();

        $this->notice = __('proxy.checked_count', $result);
    }

    /**
     * Den Proxy die Liste übernehmen lassen — und dabei fehlende Zertifikate
     * anfordern.
     *
     * Ein eigener Knopf „erneuern" wäre daneben: Caddy erneuert von selbst bei
     * zwei Dritteln der Laufzeit. Was ein Betreiber wirklich braucht, ist ein
     * zweiter Versuch, nachdem eine Ausstellung gescheitert ist — und genau das
     * ist ein Neuladen.
     */
    public function apply(UpdateChannel $channel): void
    {
        $this->authorize('site.manage');

        $this->notice = $channel->requestProxyHosts((string) auth('operator')->id())
            ? __('proxy.applying')
            : __('proxy.apply_failed');
    }

    public function render()
    {
        $hosts = ProxyHost::query()->orderBy('purpose')->orderBy('hostname')->get();

        return view('livewire.admin.proxy-hosts', [
            'hosts' => $hosts,
            // Die Übersicht in drei Zahlen. Was ein Betreiber wissen will,
            // bevor er eine Liste liest: ist etwas kaputt, und läuft etwas bald
            // ab.
            'summary' => [
                'total' => $hosts->count(),
                'valid' => $hosts->filter(fn ($h) => $h->hasCertificate() && ! $h->isExpiringSoon())->count(),
                'soon' => $hosts->filter(fn ($h) => $h->hasCertificate() && $h->isExpiringSoon())->count(),
                'failing' => $hosts->filter(fn ($h) => $h->certificate_checked_at !== null && $h->certificate_error !== null)->count(),
                'unchecked' => $hosts->filter(fn ($h) => $h->certificate_checked_at === null)->count(),
                'checkedAt' => $hosts->max('certificate_checked_at'),
            ],
            'purposes' => ProxyHost::PURPOSES,
            // Leer heißt: die Adresse des Inhabers. Eine Kontaktadresse, an die
            // die Ablaufwarnungen gehen, darf nicht leer bleiben, und die eine
            // Adresse, von der diese Installation sicher weiß, dass sie gelesen
            // wird, ist seine.
            'fallbackEmail' => $this->ownerEmail(),
        ]);
    }

    private function ownerEmail(): ?string
    {
        return Operator::query()
            ->whereHas('roles', fn ($q) => $q->where('name', 'Owner'))
            ->orderBy('id')
            ->value('email');
    }
}
