<?php

declare(strict_types=1);

namespace App\Middleware;

use App\Core\Logger;
use App\Core\MiddlewareInterface;
use App\Core\Request;
use App\Core\Session;

/**
 * AuthMiddleware — Authentication Presence and Session Timeout Guard
 *
 * Protects every portal route behind this middleware from unauthenticated
 * access and enforces the two-tier session timeout policy defined in
 * config/session.php and SPRINT2_PLANNING §5 (AUTH-004).
 *
 * Execution sequence (per request):
 *   1. Unauthenticated check — no `auth_user_id` in session → redirect /login.
 *   2. Inactivity timeout  — idle longer than SESSION_INACTIVITY_TIMEOUT (2h) → destroy + redirect.
 *   3. Absolute timeout    — session older than role limit (8h/12h/24h) → destroy + redirect.
 *   4. Touch               — update `last_active_at` for the sliding inactivity window.
 *   5. Continue pipeline   — call $next($request).
 *
 * Session timeout behaviour (§6.5 SPRINT2_EXECUTION_GUIDE):
 *   - On timeout: Session::destroy(), session_start(), Session::regenerate(false),
 *     write flash error, redirect 302 to /login.
 *   - On unauthenticated: redirect 302 to /login (no flash — normal public access).
 *
 * Timeout configuration keys (config/session.php):
 *   session.inactivity_timeout    → universal 2h (7200 s)
 *   session.timeout.admin         → super_admin  8h (28800 s)
 *   session.timeout.couple        → couple      24h (86400 s)
 *   session.timeout.crew          → crew        12h (43200 s)
 *
 * Session keys consumed (set by AuthService::bindSession() on login):
 *   auth_user_id   — int    — presence = authenticated
 *   role_snapshot  — string — used to resolve absolute timeout limit
 *   login_at       — int    — absolute session origin timestamp
 *   last_active_at — int    — updated here on each request via Session::touch()
 *
 * Architecture rules:
 *   - No SQL in this class.
 *   - No business logic — infrastructure plumbing only.
 *   - Implements MiddlewareInterface (CORE-001).
 *   - Zero constructor arguments — MiddlewarePipeline uses new $class() (frozen).
 *   - Timeout config read directly via config() helper — not via AuthService.
 *   - All redirects use header() + exit — no BaseController dependency.
 *
 * @package Inveetaire
 * @ref     SPRINT2_PLANNING.md §5, SPRINT2_EXECUTION_GUIDE.md §6.5, AUTH-004
 */
class AuthMiddleware implements MiddlewareInterface
{
    /**
     * Map role_snapshot → config/session.php timeout key.
     * `super_admin` role maps to the 'admin' config key.
     *
     * @var array<string, string>
     */
    private const ROLE_TIMEOUT_KEY = [
        'super_admin' => 'admin',
        'couple'      => 'couple',
        'crew'        => 'crew',
    ];

    /**
     * Default absolute timeout in seconds when the role key is unrecognised.
     * Matches the strictest role limit (super_admin: 8 hours).
     */
    private const DEFAULT_ABSOLUTE_TIMEOUT = 28800;

    // ─────────────────────────────────────────────────────────────────────────
    // MiddlewareInterface contract
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Assert authentication presence and enforce session timeouts.
     *
     * @param Request  $request The current HTTP request.
     * @param callable $next    The next middleware layer or controller action.
     * @return void
     */
    public function handle(Request $request, callable $next): void
    {
        // ── 1. Authentication presence check ──────────────────────────────────
        if (!Session::has('auth_user_id')) {
            $this->redirectToLogin($request);
            return;
        }

        // ── 2. Inactivity timeout check ───────────────────────────────────────
        $inactivityLimit = (int) config('session.inactivity_timeout', 1800);

        if (Session::isInactivityExpired($inactivityLimit)) {
            Logger::warning('Session expired — inactivity timeout', [
                'user_id' => (int) Session::get('auth_user_id'),
                'role'    => (string) Session::get('role_snapshot', ''),
                'ip'      => $request->getIp(),
            ]);

            // Both couple and crew roles intentionally share the unified /couple login portal (RC001-B01)
            $portal = Session::get('portal');
            $target = ($portal === 'admin') ? '/admin' : '/couple';

            $this->destroyAndRedirect($target, 'Your session has expired due to inactivity. Please sign in again.');
            return;
        }

        // ── 3. Absolute timeout check ─────────────────────────────────────────
        $role           = (string) Session::get('role_snapshot', '');
        $configKey      = self::ROLE_TIMEOUT_KEY[$role] ?? '';
        $absoluteLimit  = $configKey !== ''
            ? (int) config('session.timeout.' . $configKey, self::DEFAULT_ABSOLUTE_TIMEOUT)
            : self::DEFAULT_ABSOLUTE_TIMEOUT;

        if (Session::isAbsoluteExpired($absoluteLimit)) {
            Logger::warning('Session expired — absolute timeout reached', [
                'user_id' => (int) Session::get('auth_user_id'),
                'role'    => $role,
                'ip'      => $request->getIp(),
            ]);

            $portal = Session::get('portal');
            $target = ($portal === 'admin') ? '/admin' : '/couple';

            $this->destroyAndRedirect($target, 'Your session has expired. Please sign in again.');
            return;
        }

        // ── 4. Touch — update inactivity timestamp ────────────────────────────
        Session::touch();

        // ── 5. Continue pipeline ──────────────────────────────────────────────
        $next($request);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE — Response Helpers
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Redirect an unauthenticated request to the appropriate portal without a flash message.
     *
     * @param Request $request Current HTTP request.
     * @return void
     */
    private function redirectToLogin(Request $request): void
    {
        $path   = $request->getPath();
        $target = (strpos($path, '/app/admin') === 0) ? '/admin' : '/couple';
        header('Location: ' . url($target));
        exit;
    }

    /**
     * Destroy the expired session, open a clean anonymous session, write a
     * flash error message, and redirect to the target portal.
     *
     * @param string $target       Redirection target path.
     * @param string $flashMessage User-facing expiry notice.
     * @return void
     */
    private function destroyAndRedirect(string $target, string $flashMessage): void
    {
        Session::destroy();
        Session::start();
        Session::regenerate(false);

        $_SESSION['flash']['error'] = $flashMessage;

        header('Location: ' . url($target));
        exit;
    }
}
