<?php

namespace App\Http\Middleware;

use App\Models\LoginSession;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;

/**
 * Keeps a session row pointing at the session it describes.
 *
 * Laravel rotates the session id on every sign-in and on every regenerate().
 * A row that stored the id once stops matching within a request or two — and
 * the list would then show sessions that cannot be ended and hide the one the
 * reader is sitting in. The uuid inside the payload is what survives; the id is
 * refreshed from it here.
 *
 * Also the only thing that keeps "last seen" honest. Without it the list
 * reports the moment somebody signed in, which for a browser left open for a
 * fortnight is the least useful timestamp available.
 *
 * Written at most once a minute. This runs on every authenticated request, and
 * a write per request would turn a page of thumbnails into forty updates of the
 * same row.
 */
class TouchLoginSession
{
    private const THROTTLE_SECONDS = 60;

    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        if (! $request->hasSession()) {
            return $response;
        }

        $uuid = $request->session()->get(LoginSession::KEY);

        if (! is_string($uuid) || $uuid === '') {
            return $response;
        }

        $current = $request->session()->getId();

        // A bare update rather than a model round-trip: this is on the path of
        // every authenticated request, and the row is not needed, only moved.
        DB::table('login_sessions')
            ->where('uuid', $uuid)
            ->where(function ($q) use ($current) {
                // A CHANGED id is never throttled. Both sign-in paths raise the
                // Login event before calling session()->regenerate(), so the row
                // is written against an id that is dead a moment later. Held
                // back by the throttle, the row would point at nothing for the
                // first minute of every session — the one it describes missing
                // from the list, and "end this one" deleting a session that no
                // longer exists.
                $q->where('session_id', '!=', $current)
                    ->orWhereNull('session_id')
                    ->orWhereNull('last_seen_at')
                    ->orWhere('last_seen_at', '<=', now()->subSeconds(self::THROTTLE_SECONDS));
            })
            ->update([
                'session_id' => $current,
                'ip_address' => $request->ip(),
                'last_seen_at' => now(),
                'updated_at' => now(),
            ]);

        return $response;
    }
}
