<?php

namespace App\Http\Middleware;

use App\Support\AdminArea;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * The console answers only on its own hostname — and that hostname answers
 * only the console.
 *
 * Both halves matter. The first keeps the console off the public domains. The
 * second is what actually separates the two products: binding the console
 * routes to a hostname does not stop the CUSTOMER routes from answering there
 * as well, because they are registered without a hostname. Without this,
 * app.clupilot.com and admin.clupilot.com would still serve each other's pages
 * wherever the paths did not collide.
 *
 * 404 in both directions, never 403: a stranger must not learn that a console
 * lives here, and an operator on the wrong host should see the same nothing a
 * stranger does.
 *
 * A handful of endpoints are shared by both sides and must answer on either
 * host — Livewire's own endpoints and the authentication actions. They are
 * listed explicitly below, because the failure mode of getting that list wrong
 * is "the console is broken" rather than "the rule is wrong", and a list you
 * can read is easier to correct than a rule you have to infer.
 */
class RestrictAdminHost
{
    /**
     * Endpoints both the console and the portal need, on whichever host the
     * caller is currently on.
     *
     * `login`, `logout` and `two-factor-challenge` used to be here, from when
     * one sign-in page served both. They are gone: the console has its own at
     * `admin.login`, and leaving the portal's here is what put a registration
     * link in front of an operator — with a 404 behind it, because `register`
     * was never in this list and could not be, since the console has no
     * registration by design.
     *
     * Livewire's component endpoint is guarded separately and more strictly:
     * the console's own middleware is registered as persistent, so an action
     * posted to /livewire/update is re-checked against the component's real
     * route. Letting the path through here does not let anything through there.
     */
    private const SHARED = [
        'livewire/*',
        'up',
        // The console's own live-provisioning feed authorizes here (see
        // routes/channels.php, admin.runs) — a route Laravel registers
        // without any admin.* name, so isConsoleRoute() alone would not
        // exempt it, and in exclusive mode it would 404 on the console host
        // exactly like a stranger's request would.
        'broadcasting/auth',
    ];

    public function handle(Request $request, Closure $next): Response
    {
        // Not host-bound (development, a fresh checkout): the console keeps its
        // /admin prefix on any host and nothing is separated. Upgrading must
        // not lock anyone out of a system that was working.
        if (! AdminArea::isHostBound()) {
            return $next($request);
        }

        $isConsoleRoute = $this->isConsoleRoute($request);
        $onConsoleHost = AdminArea::covers($request->getHost());

        // The console, reached through a hostname that is not the console's.
        if ($isConsoleRoute && ! $onConsoleHost) {
            abort(404);
        }

        // The portal or the public site, reached through the console's
        // hostname. Only once exclusivity is switched on: a machine that lists
        // its own IP in ADMIN_HOSTS so the console is reachable without DNS
        // still has to serve the portal from that same address.
        if (AdminArea::isExclusive() && $onConsoleHost && ! $isConsoleRoute && ! $request->is(...self::SHARED)) {
            abort(404);
        }

        return $next($request);
    }

    /**
     * Console routes are the ones named `admin.*`.
     *
     * By name rather than by path: the path is exactly what changes between
     * host-bound and fallback mode, and a rule written against it would be
     * wrong in one of them.
     */
    private function isConsoleRoute(Request $request): bool
    {
        $name = $request->route()?->getName();

        // Covers admin.* and the alternate-host registrations (admin.viaN.*),
        // which are the same console reached by a recovery hostname.
        return $name !== null && str_starts_with($name, 'admin.');
    }
}
