<?php

require_once __DIR__ . '/../config/constants.php';

function getAuthToken(): ?string {
    $header = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
    if (empty($header) && function_exists('getallheaders')) {
        $headers = getallheaders();
        $header = $headers['Authorization'] ?? $headers['authorization'] ?? '';
    }
    if (preg_match('/Bearer\s+(.+)/i', $header, $m)) {
        return $m[1];
    }
    return null;
}

function requireAuth(): string {
    $token = getAuthToken();
    if (!$token) {
        http_response_code(401);
        echo json_encode(['error' => 'Authentication required']);
        exit;
    }
    $user = verifyToken($token);
    if (!$user) {
        http_response_code(401);
        echo json_encode(['error' => 'Invalid or expired token']);
        exit;
    }
    assertNotBanned($user['id'] ?? null);
    return $token;
}

function requireAuthUser(): array {
    $token = getAuthToken();
    if (!$token) {
        http_response_code(401);
        echo json_encode(['error' => 'Authentication required']);
        exit;
    }
    $user = verifyToken($token);
    if (!$user) {
        http_response_code(401);
        echo json_encode(['error' => 'Invalid or expired token']);
        exit;
    }
    assertNotBanned($user['id'] ?? null);
    return $user;
}

/**
 * Resolve a bearer token to a user.
 *
 * Every API call runs through here, and each live game polls twice a second, so
 * this used to make an upstream GoTrue round-trip per poll per player. A 64-board
 * tournament was generating tens of auth requests a second before a single move
 * was processed.
 *
 * Two mitigations, in order of preference:
 *   1. If the JWT signing secret is configured, verify HS256 locally — no network
 *      at all. This is what Supabase itself does to validate its own tokens.
 *   2. Otherwise fall back to the GoTrue call, but memoise the answer briefly so
 *      a burst of polls costs one request rather than dozens.
 */
function verifyToken(string $token): ?array {
    $local = verifyTokenLocally($token);
    if ($local !== null) return $local;

    $cached = authCacheGet('tok:' . hash('sha256', $token));
    if ($cached !== null) return $cached ?: null;

    $ch = curl_init(SUPABASE_AUTH . '/user');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'apikey: ' . SUPABASE_ANON_KEY,
        'Authorization: Bearer ' . $token
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if ($httpCode !== 200) {
        // Cache the rejection too, briefly, so a client stuck retrying with a
        // dead token cannot hammer the auth service.
        authCacheSet('tok:' . hash('sha256', $token), false, 30);
        return null;
    }
    $user = json_decode($response, true);
    if (is_array($user)) authCacheSet('tok:' . hash('sha256', $token), $user, AUTH_CACHE_TTL);
    return $user;
}

/**
 * Verify the JWT signature and expiry without leaving the process.
 * Returns null when no secret is configured, so the caller falls back to GoTrue.
 */
function verifyTokenLocally(string $token): ?array {
    $secret = SUPABASE_JWT_SECRET;
    if ($secret === '') return null;

    $parts = explode('.', $token);
    if (count($parts) !== 3) return null;
    [$h64, $p64, $s64] = $parts;

    $header = json_decode(base64UrlDecode($h64), true);
    if (!is_array($header) || ($header['alg'] ?? '') !== 'HS256') return null;

    $expected = hash_hmac('sha256', $h64 . '.' . $p64, $secret, true);
    if (!hash_equals($expected, base64UrlDecode($s64))) return null;

    $payload = json_decode(base64UrlDecode($p64), true);
    if (!is_array($payload) || empty($payload['sub'])) return null;
    if (isset($payload['exp']) && (int)$payload['exp'] < time()) return null;

    return ['id' => $payload['sub'], 'role' => $payload['role'] ?? 'authenticated', 'email' => $payload['email'] ?? null];
}

function base64UrlDecode(string $v): string {
    return (string)base64_decode(strtr($v, '-_', '+/') . str_repeat('=', (4 - strlen($v) % 4) % 4));
}

/**
 * Tiny short-lived cache. Uses APCu when the extension is present and falls back
 * to temp files, so it works on a stock php:apache image with no extra services.
 */
const AUTH_CACHE_TTL = 60;

function authCacheGet(string $key) {
    if (function_exists('apcu_fetch')) {
        $ok = false;
        $v = apcu_fetch($key, $ok);
        return $ok ? $v : null;
    }
    $f = sys_get_temp_dir() . '/el3ab_auth_' . hash('sha256', $key);
    if (!is_file($f)) return null;
    $raw = @file_get_contents($f);
    if ($raw === false) return null;
    $row = json_decode($raw, true);
    if (!is_array($row) || ($row['exp'] ?? 0) < time()) { @unlink($f); return null; }
    return $row['v'];
}

function authCacheSet(string $key, $value, int $ttl): void {
    if (function_exists('apcu_store')) { apcu_store($key, $value, $ttl); return; }
    $f = sys_get_temp_dir() . '/el3ab_auth_' . hash('sha256', $key);
    @file_put_contents($f, json_encode(['exp' => time() + $ttl, 'v' => $value]), LOCK_EX);
}

/**
 * Ban check, memoised for the same reason. A ban therefore takes effect within
 * a minute rather than instantly, which is an acceptable trade for removing a
 * database query from every single request.
 */
function assertNotBanned(?string $userId): void {
    if (!$userId) return;

    $cacheKey = 'ban:' . $userId;
    $cached = authCacheGet($cacheKey);
    if ($cached === null) {
        require_once __DIR__ . '/supabase.php';
        $sdb = supabaseService();
        $profiles = $sdb->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'is_banned,ban_expires_at', 'limit' => 1]);
        $cached = (is_array($profiles) && !empty($profiles) && !isset($profiles['error'])) ? $profiles[0] : [];
        authCacheSet($cacheKey, $cached, AUTH_CACHE_TTL);
    }

    if (!empty($cached['is_banned'])) {
        $banExpires = $cached['ban_expires_at'] ?? null;
        if (!$banExpires || strtotime($banExpires) > time()) {
            http_response_code(403);
            header('Content-Type: application/json');
            echo json_encode(['error' => 'Account banned', 'ban_expires_at' => $banExpires]);
            exit;
        }
    }
}

function getUserId(string $token): ?string {
    // Decode JWT payload without re-verifying (requireAuth already verified)
    $parts = explode('.', $token);
    if (count($parts) === 3) {
        $payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
        if ($payload && !empty($payload['sub'])) {
            return $payload['sub'];
        }
    }
    // Fallback: full verification
    $user = verifyToken($token);
    return $user['id'] ?? null;
}

function jsonResponse(array $data, int $code = 200): void {
    http_response_code($code);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

function jsonError(string $msg, int $code = 400): void {
    jsonResponse(['error' => $msg], $code);
}

function getInput(): array {
    $raw = file_get_contents('php://input');
    return json_decode($raw, true) ?? [];
}
