<?php

namespace App\Support;

use App\Models\DunningCase;
use App\Models\FailedCheckout;
use App\Models\Incident;
use App\Services\Deployment\UpdateChannel;
use Illuminate\Support\Facades\Cache;

/**
 * The navigation of both shells, in one place.
 *
 * The sidebar and the breadcrumb describe the same structure, and when each
 * built its own copy the breadcrumb quietly said "Übersicht" on every page —
 * the layout had no way to know which entry was current, so it fell back to the
 * first one. One definition, two readers.
 *
 * Each entry is [route name, icon, translation key, capability or null].
 */
final class Navigation
{
    /** @return array<int, array{label: ?string, items: array<int, array{0:string,1:string,2:string,3:?string}>}> */
    public static function portal(): array
    {
        return [
            ['label' => null, 'items' => [
                ['dashboard', 'gauge', 'overview', null],
                ['cloud', 'cloud', 'cloud', null],
                // The only portal entry with a condition. Start carries no
                // custom domain and cannot buy one, so the tab would lead
                // straight to the 403 its page raises.
                ['domain', 'globe', 'domain', 'use-custom-domain'],
                ['users', 'users', 'users', null],
                ['backups', 'database', 'backups', null],
                // Route heisst 'portal.security' statt bloss 'security' — der
                // Pfad '/security' gehoert schon der oeffentlichen
                // Aufklaerungsseite (routes/web.php), siehe deren Kommentar.
                ['portal.security', 'shield', 'security', null],
            ]],
            ['label' => __('dashboard.nav_group.contract'), 'items' => [
                ['billing', 'box', 'billing', null],
                ['invoices', 'receipt', 'invoices', null],
                ['settings', 'settings', 'settings', null],
                ['support', 'life-buoy', 'support', null],
            ]],
        ];
    }

    /**
     * Die Konsole, geordnet nach dem, was man mit ihr tut.
     *
     * Es waren achtundzwanzig Eintraege in sieben Gruppen, und „System" war der
     * Platz fuer alles, was sonst nirgends hinpasste: Mail-Einrichtung neben
     * einem Rechtsdokument, neben den persoenlichen Kontoeinstellungen, neben
     * der Mitarbeiterverwaltung — und ganz unten die Seite, die sagt, was
     * liegt. Der Betreiber hat es so beschrieben: „offene Punkte ist der letzte
     * Punkt, dann Rolle drueber und Einstellungen wieder drueber".
     *
     * Zwei Regeln ordnen es jetzt. Was man einmal einrichtet, verlaesst die
     * Leiste in den Einrichtungsbereich (admin.setup). Und die drei Seiten, auf
     * denen etwas auf jemanden WARTET, stehen ganz oben, mit einer Zahl daneben
     * — das ist die Frage, mit der man eine Konsole oeffnet.
     *
     * Jede Seite steht genau EINMAL. Stoerungen sind aus „Betrieb" nach oben
     * gezogen und Zahlungsprobleme aus „Geld"; sie sind dort nicht zusaetzlich
     * geblieben. Eine Seite an zwei Stellen ist ihre eigene Verwirrung.
     *
     * Ein Eintrag ist [Route, Symbol, Uebersetzungsschluessel, Berechtigung],
     * optional gefolgt vom Schluessel, unter dem attentionCounts() seine Zahl
     * fuehrt.
     *
     * @return array<int, array{label: ?string, items: array<int, array{0:string,1:string,2:string,3:mixed,4?:string}>}>
     */
    public static function console(): array
    {
        return [
            // Allein und ohne Ueberschrift: die Seite, auf der man landet.
            ['label' => null, 'items' => [
                ['admin.overview', 'gauge', 'overview', null],
            ]],
            // Wo etwas auf jemanden wartet. Der einzige Block, der eine Zahl
            // traegt — und der einzige, der nach Dringlichkeit sortiert ist
            // statt nach Gegenstand.
            ['label' => __('admin.nav_group.attention'), 'items' => [
                // Ohne Berechtigung: wer die Konsole oeffnen darf, soll wissen,
                // worauf er sich verlassen kann und worauf nicht.
                ['admin.open-work', 'alert-triangle', 'open_work', null, 'open_work'],
                ['admin.incidents', 'bell', 'incidents', null, 'incidents'],
                ['admin.payment-problems', 'alert-triangle', 'payment_problems', 'billing.manage', 'payment_problems'],
            ]],
            // Wer bei uns ist und was er hat.
            ['label' => __('admin.nav_group.customers'), 'items' => [
                ['admin.customers', 'users', 'customers', null],
                ['admin.instances', 'box', 'instances', null],
            ]],
            // Die Maschinen und ihre Adressen — der Bestand, den man ansieht.
            ['label' => __('admin.nav_group.fleet'), 'items' => [
                ['admin.hosts', 'server', 'hosts', null],
                ['admin.datacenters', 'database', 'datacenters', null],
                ['admin.capacity', 'database', 'capacity', 'hosts.manage'],
                // Hostnamen und ihre Zertifikate: was hier steht, entscheidet,
                // ob eine Adresse ANTWORTET. Kein Einrichten, sondern Bestand.
                ['admin.proxy-hosts', 'globe', 'proxy_hosts', 'site.manage'],
                // Eine LISTE VON GEGENSTELLEN, die man beim Arbeiten ansieht,
                // so wie die Hostliste — nicht etwas, das man einrichtet.
                ['admin.vpn', 'shield', 'vpn', null],
            ]],
            // Was gerade laeuft. Vorgaenge, kein Bestand. Stoerungen standen
            // hier und stehen jetzt oben: sie sind kein Vorgang, den man
            // verfolgt, sondern einer, der jemanden braucht.
            ['label' => __('admin.nav_group.operations'), 'items' => [
                ['admin.provisioning', 'activity', 'provisioning', null],
                ['admin.maintenance', 'alert-triangle', 'maintenance', null],
                // Laeuft, statt eingerichtet zu werden — deshalb hier und nicht
                // im Einrichtungsbereich neben admin.mail.
                ['admin.mail-pace', 'gauge', 'mail_pace', 'mail.manage'],
            ]],
            // Alles, wo Geld drinsteht.
            ['label' => __('admin.nav_group.billing'), 'items' => [
                ['admin.revenue', 'trending-up', 'revenue', null],
                ['admin.invoices', 'file-text', 'invoices', 'site.manage'],
                // Eigener Eintrag, kein Abschnitt der Einstellungen: was hier
                // gesetzt wird, steht auf einem Rechtsdokument, und neben dem
                // Sichtbarkeitsschalter aendert das jemand im Vorbeigehen.
                ['admin.finance', 'receipt', 'finance', 'site.manage'],
            ]],
            // Was geschrieben wird — gelesen beim Antworten. Die Vorlagen und
            // ihre Vorschau sind dagegen etwas, das man einmal schreibt: sie
            // stehen im Einrichtungsbereich.
            ['label' => __('admin.nav_group.post'), 'items' => [
                ['admin.inbox', 'mail', 'inbox', 'customers.manage'],
                ['admin.mail-log', 'send', 'mail_log', 'customers.manage'],
            ]],
            // Ohne Ueberschrift und ganz unten: eine Tuer, keine Gruppe.
            ['label' => null, 'items' => [
                ['admin.setup', 'settings', 'setup', null],
            ]],
        ];
    }

    /**
     * Die Zahlen fuer den Block „Braucht dich".
     *
     * Diese Leiste rendert auf JEDER Konsolenseite. Ohne Zwischenspeicher
     * waeren das zwei Abfragen je Seitenaufruf — eine Abgabe, die man spaeter
     * sucht, wenn die Seiten langsam werden. Eine Minute ist kurz genug, dass
     * niemand eine geloeste Stoerung noch lange gezaehlt sieht, und lang genug,
     * dass ein Klick durch fuenf Seiten die Datenbank einmal fragt statt zehnmal.
     *
     * „Offene Punkte" kostet ohnehin nichts: das Register liegt im Code.
     *
     * @return array<string, int>
     */
    public static function attentionCounts(): array
    {
        return Cache::remember('nav.attention', now()->addMinute(), fn () => [
            'open_work' => count(OpenWork::all()),
            'incidents' => Incident::query()->whereNull('resolved_at')->count(),
            // Beides, weil die Seite beides zeigt: eine offene Mahnung und ein
            // gescheiterter Bezahlvorgang sind zwei Wege zu demselben Problem.
            'payment_problems' => DunningCase::query()->whereNull('settled_at')->count()
                + FailedCheckout::query()->whereNull('resolved_at')->count(),
            // Die Aktualisierung, obwohl sie in keinem Eintrag der Leiste
            // steht: sie steht im FUSS, neben der laufenden Version.
            //
            // Der Betreiber hat es so gemeldet: „Einstellungen ist in der
            // Einrichtung, wegen den Versionsupdates — man sieht es nicht,
            // ohne genau hinzuklicken." Die Seite dorthin zu verschieben war
            // richtig (ihr Konto-Teil gehoert dahin), die Folge nicht: eine
            // verfuegbare Aktualisierung ist nichts, wonach man sucht.
            //
            // Hier statt in einem eigenen Eintrag, weil die Version im Fuss
            // ohnehin schon steht. Ein vierter Eintrag unter „Braucht dich"
            // haette dieselbe Auskunft an einer zweiten Stelle wiederholt.
            'update' => app(UpdateChannel::class)->state()['available'] ? 1 : 0,
        ]);
    }

    /**
     * Der Einrichtungsbereich: die neun Seiten, die man einmal einrichtet.
     *
     * Sie stehen hier und nicht im Livewire-Bauteil, das sie zeichnet — aus
     * demselben Grund, aus dem die Seitenleiste hier steht: es ist Navigation,
     * und wer sie liest, sind drei (die Kachelseite, die Markierung „du bist
     * hier" und die Brotkrume). Drei Leser, eine Liste.
     *
     * Dieselbe Form wie ein Eintrag der Leiste, und dieselbe Regel: ein Array
     * bei der Berechtigung heisst „eine davon genuegt", nie „alle".
     *
     * @return array<string, array<int, array{0:string,1:string,2:string,3:mixed}>>
     */
    public static function setup(): array
    {
        return [
            // Womit und woraus dieses Haus schreibt.
            'delivery' => [
                ['admin.mail', 'mail', 'mail', 'mail.manage'],
                ['admin.templates', 'file-text', 'templates', 'customers.manage'],
                ['admin.mail.preview', 'send', 'mail_preview', 'mail.manage'],
            ],
            // Was verkauft wird.
            'offer' => [
                ['admin.plans', 'tag', 'plans', 'plans.manage'],
            ],
            // Wer hereindarf und womit dieses Haus nach aussen spricht.
            'access' => [
                ['admin.roles', 'users', 'roles', 'staff.manage'],
                ['admin.settings', 'settings', 'settings', null],
                ['admin.integrations', 'plug', 'integrations', ['hosts.manage', 'secrets.manage']],
            ],
            // Der Zustand des Hauses selbst.
            'house' => [
                ['admin.readiness', 'shield-check', 'readiness', ['hosts.manage', 'secrets.manage']],
                ['admin.dpa', 'file-text', 'dpa', 'dpa.manage'],
            ],
        ];
    }

    /**
     * Ist dieser Eintrag der, auf dem man gerade steht?
     *
     * Fuer alles ausser dem Einrichtungsbereich ist das die Route selbst. Der
     * Einrichtungsbereich bleibt zusaetzlich markiert, solange man auf einer
     * der Seiten steht, die er sammelt — sonst verliert die Leiste auf allen
     * neun verschobenen Seiten ihr „du bist hier", und man steht in einer
     * Konsole, die nicht mehr sagt, wo man ist. (Codex R15, P2 am Umbau.)
     */
    public static function isCurrent(string $route, bool $console = false): bool
    {
        if ($console ? AdminArea::routeIs($route) : request()->routeIs($route)) {
            return true;
        }

        if ($console && $route === 'admin.setup') {
            foreach (self::setup() as $kacheln) {
                foreach ($kacheln as [$ziel]) {
                    if (AdminArea::routeIs($ziel)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * The label of the entry the current request belongs to.
     *
     * Matched on the route rather than the path, because the console's path
     * changes between host-bound and fallback mode while its route names do
     * not. Null when the page is not in the navigation at all — a detail view,
     * a modal — and the caller then supplies its own title.
     */
    public static function currentLabel(bool $console = false): ?string
    {
        $prefix = $console ? 'admin.nav.' : 'dashboard.nav.';

        foreach ($console ? self::console() : self::portal() as $group) {
            foreach ($group['items'] as [$route, , $key]) {
                $matches = $console
                    ? AdminArea::routeIs($route)
                    : request()->routeIs($route);

                if ($matches) {
                    return __($prefix.$key);
                }
            }
        }

        // Die neun verschobenen Seiten stehen nicht mehr in der Leiste. Ohne
        // diesen zweiten Blick verloeren sie ihre Brotkrume und die Konsole
        // sagte auf ihnen nur noch „Konsole" — derselbe Schaden wie die
        // fehlende Markierung, aus derselben Ursache.
        if ($console) {
            foreach (self::setup() as $kacheln) {
                foreach ($kacheln as [$route, , $key]) {
                    if (AdminArea::routeIs($route)) {
                        return __($prefix.$key);
                    }
                }
            }
        }

        return null;
    }
}
