<?php

namespace App\Support\Readiness;

use App\Http\Middleware\RestrictConsoleNetwork;

/**
 * Was von aussen offensteht — und was offenstehen soll.
 *
 * Die anderen Gruppen in dieser Datei fragen, ob etwas EINGERICHTET ist. Diese
 * fragt, ob etwas ZU ist. Der Unterschied ist wichtig: eine fehlende
 * Einrichtung faellt beim ersten Versuch auf, eine offene Tuer nie — bis sie
 * jemand benutzt.
 *
 * Alle drei Punkte hier sind aus einer Durchsicht am 2026-08-03 entstanden, und
 * zwei davon standen auf der echten Anlage wirklich so.
 */
final class SecurityChecks
{
    public const GROUP = 'security';

    /**
     * @return list<Check>
     */
    public static function all(): array
    {
        return [
            new Check(
                key: 'security.console_restricted',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.security.console_restricted'),
                breaks: __('readiness.security.console_restricted_breaks'),
                tab: 'security',
                satisfied: RestrictConsoleNetwork::isRestricted(),
            ),

            new Check(
                key: 'security.trusted_ranges_clean',
                group: self::GROUP,
                severity: Check::SEVERITY_WARNING,
                label: __('readiness.security.trusted_ranges'),
                breaks: __('readiness.security.trusted_ranges_breaks'),
                tab: 'security',
                // Alles, was ueber das Management-Netz und die eigene Maschine
                // hinausgeht, ist von Hand in die .env geschrieben worden — und
                // steht damit an einer Stelle, die der Betreiber in der Konsole
                // nicht aendern kann. Genau so stand eine Bueroadresse dort:
                // beim naechsten Anschlusswechsel waere das ein Aussperren
                // gewesen. Sie gehoert in die Liste, die man pflegen kann.
                satisfied: array_diff(
                    (array) config('admin_access.trusted_ranges', []),
                    (array) config('admin_access.structural_ranges', []),
                ) === [],
            ),

            new Check(
                key: 'security.ports_loopback',
                group: self::GROUP,
                severity: Check::SEVERITY_WARNING,
                label: __('readiness.security.ports_loopback'),
                breaks: __('readiness.security.ports_loopback_breaks'),
                tab: 'env',
                satisfied: self::onlyLoopback(),
            ),
        ];
    }

    /**
     * Sind die Anwendungs-Ports nur auf der eigenen Maschine erreichbar?
     *
     * Docker traegt veroeffentlichte Ports VOR der Firewall ein. Ein Dienst auf
     * `0.0.0.0` ist damit aus dem Internet erreichbar, auch wenn ufw geschlossen
     * aussieht — und wer ihn direkt anspricht, geht am Reverse Proxy vorbei und
     * damit an dessen Zugangsliste und an TLS.
     *
     * Ohne Angabe gilt in docker-compose.yml die Schleife (`127.0.0.1:8080`),
     * dann ist alles in Ordnung. Ein blosser Port ohne Adresse — `APP_PORT=80` —
     * bedeutet bei Docker ausdruecklich ALLE Adressen; das ist der Fall, den
     * diese Pruefung sucht.
     */
    private static function onlyLoopback(): bool
    {
        foreach (['APP_PORT', 'REVERB_HOST_PORT'] as $name) {
            $wert = trim((string) env($name, ''));

            if ($wert === '') {
                continue; // Vorgabe aus docker-compose.yml: Schleife.
            }

            if (! str_starts_with($wert, '127.0.0.1:') && ! str_starts_with($wert, 'localhost:')) {
                return false;
            }
        }

        return true;
    }
}
