<?php
// =============================================================
//  api/v1/_bootstrap.php
//
//  Required at the top of every api/v1/*.php endpoint file. Sets up
//  JSON-only error handling (same pattern as app/ai_proxy.php), CORS,
//  the shared DB/permission/account plumbing, and api_authenticate() —
//  the token-auth equivalent of the web app's session login.
//
//  api_authenticate() hydrates $_SESSION exactly like a web login would
//  (hydrateRequestContext(), extracted from include/session_setup.php in
//  this same phase) so PermissionManager, AuthMiddleware's JSON guards,
//  and every function in include/scope_helper.php work UNMODIFIED for
//  API requests — one authorization implementation, not two. Nothing
//  here persists that session back to the client as a cookie the mobile
//  app is expected to manage; a fresh Bearer token drives every request.
// =============================================================

ob_start();
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
// Native mobile HTTP clients don't enforce CORS, but this keeps the API
// callable from a browser (Postman/Flutter-web) during development too.
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit();
}

require_once __DIR__ . '/_response.php';

// ── Global safety net — every endpoint ALWAYS returns valid JSON, even if
// something unexpected throws (mirrors app/ai_proxy.php's handler). ─────────
set_exception_handler(function (Throwable $e) {
    error_log('STH NRMS API: uncaught exception — ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
    if (ob_get_level() > 0) ob_clean();
    Api::error(500, 'ServerError', 'Something went wrong. Please try again.');
});
set_error_handler(function ($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) return false; // respect @-suppression
    throw new ErrorException($message, 0, $severity, $file, $line);
});

require_once __DIR__ . '/../../include/config.php';
require_once __DIR__ . '/../../include/db_connection.php'; // provides $conn (mysqli)
require_once __DIR__ . '/../../include/PermissionManager.php';
require_once __DIR__ . '/../../include/AuthMiddleware.php';
require_once __DIR__ . '/../../include/AccountRepository.php';
require_once __DIR__ . '/../../include/session_setup.php';
require_once __DIR__ . '/../../include/ApiTokenManager.php';
require_once __DIR__ . '/../../include/PendingAuthToken.php';
require_once __DIR__ . '/../../include/scope_helper.php';
require_once __DIR__ . '/../../include/settings_helper.php';

// $_SESSION here is a per-request context bag only (see file header) — same
// cookie params as app/ai_proxy.php for consistency, though nothing depends
// on this session surviving to a second request.
if (session_status() === PHP_SESSION_NONE) {
    session_set_cookie_params([
        'lifetime' => 0,
        'path'     => '/',
        'secure'   => isset($_SERVER['HTTPS']),
        'httponly' => true,
        'samesite' => 'Strict',
    ]);
    session_start();
}

$conn->set_charset('utf8mb4');

$perm_manager  = new PermissionManager($conn);
$auth          = new AuthMiddleware($perm_manager);
$account_repo  = new AccountRepository($conn);
$token_manager = new ApiTokenManager($conn);

/**
 * Reads the Authorization: Bearer header, resolves it to an account, and
 * hydrates $_SESSION via hydrateRequestContext() — so every existing
 * permission/scope helper (PermissionManager, AuthMiddleware, scope_helper.php)
 * works exactly as it does for the web app.
 *
 * Sends 401 JSON and exits if the header is missing/malformed, the token is
 * invalid/expired/revoked, or the account is no longer Active.
 */
function api_authenticate(): Account
{
    global $conn, $perm_manager, $account_repo, $token_manager;

    $header = $_SERVER['HTTP_AUTHORIZATION']
        ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
        ?? '';

    if (!preg_match('/^Bearer\s+(\S+)$/i', trim($header), $m)) {
        Api::error(401, 'Unauthorized', 'Missing or malformed Authorization header');
    }

    $resolved = $token_manager->resolve($m[1]);
    if (!$resolved) {
        Api::error(401, 'Unauthorized', 'Invalid, expired, or revoked token');
    }

    $account = $account_repo->findById($resolved['account_table'], $resolved['account_id']);
    if (!$account || $account->getStatus() !== 'Active') {
        Api::error(401, 'Unauthorized', 'Account not found or inactive');
    }

    hydrateRequestContext($conn, $perm_manager, $account->toArray(), $account->getTable(), $account->getUserType());
    // Endpoints that need the raw token again (e.g. logout) can read it here.
    $_SESSION['api_bearer_token'] = $m[1];

    return $account;
}

/** Convenience: the raw bearer token for the current request (set by api_authenticate()). */
function api_current_token(): ?string
{
    return $_SESSION['api_bearer_token'] ?? null;
}

/**
 * Gates a staff-only endpoint by usertype (Admin, Preceptor, Nurse_Manager, …).
 *
 * Deliberately NOT $auth->requireRole() / AuthMiddleware::hasRole(): those
 * call PermissionManager::getUserRole($userId), which looks the role up via
 * `JOIN users u ON u.role_id = r.id WHERE u.id = ?` — i.e. it always queries
 * the `users` table by that raw numeric ID. `users`, `rotation_nurses`, and
 * `clinical_students` each run their own independent auto-increment
 * sequence starting at 1 (see docs/account-unification-plan.md), so a
 * Clinical_Student or Rotation_Nurse whose ID happens to match a real
 * `users` row's ID would silently inherit THAT staff member's role from
 * requireRole() — this was caught live during Phase A3 smoke testing (a
 * student with id=1 was granted the id=1 Admin's access on a staff-only
 * endpoint) before it ever reached deployment. Checking the already-
 * table-scoped Account object's own resolved user_type instead of
 * re-deriving one from a bare, ambiguous integer ID closes that off
 * entirely — there's no lookup left to collide.
 */
function api_require_staff_role(Account $account, array $allowedUserTypes): void
{
    if ($account->getTable() !== 'users' || !in_array($account->getUserType(), $allowedUserTypes, true)) {
        Api::error(403, 'Forbidden', 'This action requires role: ' . implode(' or ', $allowedUserTypes));
    }
}

/**
 * Gates a staff-only endpoint by named permission — the API-side equivalent
 * of include/auth_guard.php's requirePermission(), which every staff-facing
 * web page (e.g. app/nurse_attendance.php, app/clinical_attendance.php)
 * already uses. Reads $_SESSION['permissions'], populated by
 * hydrateRequestContext() inside api_authenticate(), so it reflects the same
 * role-permissions + individual-override logic as the web app
 * (PermissionManager::getUserPermissions()) — not a separate mobile-only
 * rule set that could drift from it.
 */
function api_require_permission(string $permission): void
{
    if (!PermissionManager::sessionCan($permission)) {
        Api::error(403, 'Forbidden', 'This action requires permission: ' . $permission);
    }
}

/**
 * Same as api_require_permission() but passes if the caller holds ANY of
 * the given permissions — for endpoints the web app itself gates on more
 * than one permission name (e.g. nurse assessment pages check the generic
 * 'assess_students' but a stage-specific role like Nurse_Manager only
 * holds 'assess_nurses_stage2', not the generic one — see the fix to
 * app/nurse_assessment_form.php / nurse_assessments.php in the same
 * commit this helper was added in).
 */
function api_require_any_permission(array $permissions): void
{
    if (!PermissionManager::sessionCanAny($permissions)) {
        Api::error(403, 'Forbidden', 'This action requires one of: ' . implode(', ', $permissions));
    }
}
