<?php

namespace App\Http\Middleware;

use App\Support\AdminArea;
use App\Support\Settings;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpFoundation\Response;

/**
 * Hides the marketing site and the customer portal while the product is still
 * being built, without hiding it from us.
 *
 * Switched from the console (site.public). Anyone coming through the management
 * VPN, and any signed-in operator, sees the real thing; everyone else — crawlers
 * included — gets a placeholder.
 *
 * Answers 503 with Retry-After and X-Robots-Tag rather than 200: a 200 would
 * invite search engines to index the placeholder as the site's content, and
 * getting that out of an index again is much harder than keeping it out.
 */
class PublicSiteGate
{
    /**
     * Paths that must keep working regardless: the console itself (otherwise the
     * switch could not be flipped back), Stripe's webhook, and the health check.
     *
     * Livewire is deliberately NOT in this list. Its endpoint is shared by the
     * console and the portal, so exempting it would leave a signed-in customer
     * able to drive portal components — including billing — while the portal is
     * supposed to be offline. Operators pass the check below anyway, which is
     * what the console actually needs.
     *
     * Nor is /login, and that has a consequence worth knowing before you hide
     * the site: /admin sends a guest to /login, which is not the console, so
     * SIGNING IN while hidden needs the request to come from a trusted range —
     * the management VPN, or an address put in TRUSTED_RANGES for the initial
     * setup. Exempting the login flow by hostname instead would mean trusting a
     * Host header, which the caller chooses, and one forged header would unhide
     * the entire portal.
     */
    private const ALWAYS_ALLOWED = ['webhooks/*', 'up', 'robots.txt'];

    public function handle(Request $request, Closure $next): Response
    {
        // The console is exempt wherever it currently lives. This used to be
        // the literal paths 'admin' and 'admin/*', which stops matching the
        // moment the console moves to the root of its own hostname — and the
        // result would be 503 on the sign-in page of the console, which is the
        // only place the switch can be turned back on. Asking AdminArea keeps
        // the exemption attached to the console rather than to a path that
        // happened to be true once.
        if (AdminArea::isConsole($request) || $request->is(...self::ALWAYS_ALLOWED) || Settings::bool('site.public', true)) {
            return $next($request);
        }

        if ($this->isDownload($request)) {
            return $next($request);
        }

        if ($this->fromManagementNetwork($request) || $this->isActiveOperator()) {
            return $next($request);
        }

        return response()->view('coming-soon', [], 503)->withHeaders([
            'Retry-After' => 3600,
            'X-Robots-Tag' => 'noindex, nofollow, noarchive',
            'Cache-Control' => 'no-store',
        ]);
    }

    /**
     * Die Datei-Domain bleibt erreichbar, auch solange die Website verborgen ist.
     *
     * Sonst bricht jede Host-Übernahme, und zwar unsichtbar: ein Server im
     * Rettungssystem holt das Archiv mit `curl … | tar xz`, bekäme hier die
     * 503-Platzhalterseite und schöbe sie in `tar`. Der Betreiber sähe einen
     * Entpackfehler auf einer Maschine, an die er nur noch über die
     * Anbieterkonsole kommt — und nichts davon deutete auf einen Schalter in
     * der Konsole hin. Er ist per Definition weder in einem verwalteten Netz
     * noch angemeldet: das ist ja gerade der Zustand, den er verlassen will.
     *
     * Über den ROUTENNAMEN, nicht über den Pfad: die Route existiert nur, wenn
     * die Domain gebunden hat. Der Docblock oben warnt zu Recht davor, per
     * Hostname auszunehmen — dort ginge es aber um das gesamte Portal, und ein
     * gefälschter Host-Header hätte es aufgedeckt. Hier stehen zwei Routen
     * dahinter: die Rechtsdokumente, die ohnehin öffentlich sind, und das
     * Archiv, das ohne gültigen Einmal-Code mit 404 antwortet. Wer den Header
     * fälscht, gewinnt damit nichts, was er nicht auch nach dem Start hätte.
     */
    private function isDownload(Request $request): bool
    {
        return in_array($request->route()?->getName(), ['bootstrap.archive', 'files.public'], true);
    }

    private function fromManagementNetwork(Request $request): bool
    {
        $ranges = (array) config('admin_access.trusted_ranges', []);

        return $ranges !== [] && IpUtils::checkIp((string) $request->ip(), $ranges);
    }

    /**
     * Same standard as EnsureAdmin: an active operator holding a console role.
     * A bare `check()` stays true for a disabled operator's live session —
     * disabling someone did not sign them out, so it never stopped this gate
     * from waving them through indefinitely.
     *
     * Deliberately checked, not enforced by logging out: this gate only
     * decides what ONE request sees, and destroying the session would ripple
     * into whatever else that session is doing (a concurrent console tab,
     * mid-task) — a much bigger side effect than a visibility check exists to
     * have. EnsureAdmin sets the precedent: it also 403s a disabled operator
     * per request without touching their session.
     */
    private function isActiveOperator(): bool
    {
        $operator = Auth::guard('operator')->user();

        return $operator !== null && $operator->isActive() && $operator->can('console.view');
    }
}
