<?php
/**
 * lib/api.php
 *
 * Helpers for the token-authenticated mobile JSON API (/api/v1).
 *
 * The web app authenticates with PHP session cookies + CSRF. Native mobile
 * clients can't ride those cleanly, so the API uses stateless Bearer tokens
 * (see the api_tokens table, migration 005). This file is the only place that
 * understands tokens; individual endpoints just call api_auth($pdo).
 *
 * Each endpoint does:
 *     require_once __DIR__ . '/../../config.php';   // $pdo, env, libs
 *     require_once __DIR__ . '/../../lib/api.php';
 *     api_require_method('POST');
 *     $user = api_auth($pdo);                        // 401s on bad token
 *
 * Blocked from direct web access via the lib/.htaccess deny rule.
 */

declare(strict_types=1);

// ---------------------------------------------------------------------------
// JSON output
// ---------------------------------------------------------------------------

function api_json($data, int $status = 200): void
{
    if (!headers_sent()) {
        http_response_code($status);
        header('Content-Type: application/json; charset=utf-8');
        // These responses are per-user and must never be cached by proxies.
        header('Cache-Control: no-store');
    }
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

function api_ok(array $data = []): void
{
    api_json(array_merge(['ok' => true], $data), 200);
}

/** Emit an error envelope and stop. `$extra` is merged into the body. */
function api_error(string $code, int $status = 400, array $extra = []): void
{
    api_json(array_merge(['ok' => false, 'error' => $code], $extra), $status);
}

// ---------------------------------------------------------------------------
// Request helpers
// ---------------------------------------------------------------------------

/** Decode the JSON request body to an array (empty array if absent/invalid). */
function api_body(): array
{
    $raw  = file_get_contents('php://input');
    $body = json_decode($raw ?: '[]', true);
    return is_array($body) ? $body : [];
}

function api_require_method(string $method): void
{
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== strtoupper($method)) {
        api_error('method_not_allowed', 405);
    }
}

function api_client_ip(): string
{
    return (string) ($_SERVER['REMOTE_ADDR'] ?? '');
}

// ---------------------------------------------------------------------------
// Tokens
// ---------------------------------------------------------------------------

/** Opaque 256-bit token, delivered to the client once at login. */
function api_generate_token(): string
{
    return bin2hex(random_bytes(32)); // 64 hex chars
}

/** We store only the hash; a DB leak therefore can't be replayed as tokens. */
function api_token_hash(string $token): string
{
    return hash('sha256', $token);
}

/**
 * Pull the Bearer token out of the request. Shared hosting (CGI/FastCGI) is
 * inconsistent about where the Authorization header lands, so we check every
 * plausible source. The api/.htaccess also copies it into HTTP_AUTHORIZATION.
 */
function api_extract_bearer(): ?string
{
    $candidates = [];
    foreach (['HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION'] as $k) {
        if (!empty($_SERVER[$k])) {
            $candidates[] = $_SERVER[$k];
        }
    }
    if (function_exists('getallheaders')) {
        foreach (getallheaders() as $name => $val) {
            if (strcasecmp((string)$name, 'Authorization') === 0 && $val !== '') {
                $candidates[] = $val;
            }
        }
    }
    foreach ($candidates as $hdr) {
        if (preg_match('/Bearer\s+(\S+)/i', (string)$hdr, $m)) {
            return $m[1];
        }
    }
    return null;
}

/**
 * Authenticate the request via Bearer token. Returns a user array shaped like
 * current_user() plus 'token_id', or emits a 401 and exits.
 *
 * Side effect: populates $_SESSION['user_id'] / ['role'] so the existing
 * audit_log() helper attributes mobile actions to the right user.
 */
function api_auth(PDO $pdo): array
{
    $token = api_extract_bearer();
    if ($token === null || $token === '') {
        api_error('unauthorized', 401);
    }

    $stmt = $pdo->prepare("
        SELECT t.id AS token_id, t.user_id, t.expires_at, t.revoked_at,
               u.username, u.full_name, u.role, u.facility_id, u.active
        FROM api_tokens t
        JOIN users u ON u.id = t.user_id
        WHERE t.token_hash = ?
        LIMIT 1
    ");
    $stmt->execute([api_token_hash($token)]);
    $row = $stmt->fetch();

    if (!$row || (int) $row['active'] !== 1) {
        api_error('unauthorized', 401);
    }
    if ($row['revoked_at'] !== null) {
        api_error('token_revoked', 401);
    }
    if ($row['expires_at'] !== null && strtotime((string) $row['expires_at']) < time()) {
        api_error('token_expired', 401);
    }

    // Best-effort last-seen stamp; never fail the request over it.
    try {
        $pdo->prepare("UPDATE api_tokens SET last_used_at = NOW() WHERE id = ?")
            ->execute([(int) $row['token_id']]);
    } catch (Throwable $e) {
        error_log('api_auth last_used_at: ' . $e->getMessage());
    }

    $user = [
        'id'          => (int) $row['user_id'],
        'username'    => $row['username'],
        'full_name'   => $row['full_name'],
        'role'        => $row['role'],
        'facility_id' => $row['facility_id'] !== null ? (int) $row['facility_id'] : null,
        'token_id'    => (int) $row['token_id'],
    ];

    // Let audit_log() (which reads $_SESSION) attribute this request correctly.
    $_SESSION['user_id'] = $user['id'];
    $_SESSION['role']    = $user['role'];

    return $user;
}

// ---------------------------------------------------------------------------
// Grading helpers
//
// Mirrors get_bands()/pick_grade() from grade.php. Kept here as a tiny private
// copy so the API never has to include grade.php (which renders HTML), and so
// the web grading page stays untouched.
// ---------------------------------------------------------------------------

function api_get_bands(PDO $pdo, int $formId): array
{
    $s = $pdo->prepare('SELECT * FROM grade_bands WHERE form_id = ? ORDER BY sort_order');
    $s->execute([$formId]);
    return $s->fetchAll();
}

function api_pick_grade(array $bands, float $percent): string
{
    foreach ($bands as $b) {
        if ($percent >= (float) $b['min_percent'] && $percent <= (float) $b['max_percent']) {
            return (string) $b['label'];
        }
    }
    return 'Uncategorised';
}
