<?php

/**
 * Minimal OpenID Connect support for Keycloak-backed website login.
 *
 * Required .env values:
 * - QDB_KEYCLOAK_ISSUER, e.g. https://.../realms/...
 * - QDB_KEYCLOAK_CLIENT_ID
 * - QDB_KEYCLOAK_CLIENT_SECRET
 *
 * Optional:
 * - QDB_KEYCLOAK_REDIRECT_URI (defaults to current /api/auth/keycloak-callback)
 * - QDB_KEYCLOAK_USERNAME_CLAIM (defaults to preferred_username)
 * - QDB_KEYCLOAK_DISPLAY_NAME (defaults to University Konstanz)
 */

function qdb_keycloak_config(): array
{
    $issuer = rtrim((string)(qdb_env_get('QDB_KEYCLOAK_ISSUER') ?? ''), '/');
    $clientId = (string)(qdb_env_get('QDB_KEYCLOAK_CLIENT_ID') ?? '');
    $clientSecret = (string)(qdb_env_get('QDB_KEYCLOAK_CLIENT_SECRET') ?? '');
    return [
        'issuer' => $issuer,
        'client_id' => $clientId,
        'client_secret' => $clientSecret,
        'redirect_uri' => (string)(qdb_env_get('QDB_KEYCLOAK_REDIRECT_URI') ?? qdb_keycloak_default_redirect_uri()),
        'username_claim' => (string)(qdb_env_get('QDB_KEYCLOAK_USERNAME_CLAIM') ?? 'preferred_username'),
        'display_name' => (string)(qdb_env_get('QDB_KEYCLOAK_DISPLAY_NAME') ?? 'University Konstanz'),
    ];
}

function qdb_keycloak_is_configured(?array $config = null): bool
{
    $config ??= qdb_keycloak_config();
    return $config['issuer'] !== '' && $config['client_id'] !== '' && $config['client_secret'] !== '';
}

function qdb_keycloak_default_redirect_uri(): string
{
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
    $scheme = $https ? 'https' : 'http';
    $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
    $script = $_SERVER['SCRIPT_NAME'] ?? '/api/index.php';
    $base = rtrim(str_replace('\\', '/', dirname($script)), '/');
    return $scheme . '://' . $host . $base . '/auth/keycloak-callback';
}

function qdb_keycloak_frontend_url(): string
{
    $configured = (string)(qdb_env_get('QDB_KEYCLOAK_FRONTEND_URL') ?? '');
    if ($configured !== '') {
        return $configured;
    }
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
    $scheme = $https ? 'https' : 'http';
    $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
    $script = $_SERVER['SCRIPT_NAME'] ?? '/api/index.php';
    $apiBase = rtrim(str_replace('\\', '/', dirname($script)), '/');
    $root = preg_replace('#/api$#', '', $apiBase) ?: '';
    return $scheme . '://' . $host . $root . '/website/index.html';
}

function qdb_keycloak_state_signing_key(): string
{
    $secret = qdb_env_get('QDB_KEYCLOAK_STATE_SECRET')
        ?? qdb_env_get('QDB_MASTER_KEY')
        ?? qdb_env_get('QDB_KEYCLOAK_CLIENT_SECRET')
        ?? '';
    if ($secret === '') {
        throw new RuntimeException('No signing secret available for Keycloak state');
    }
    return $secret;
}

function qdb_keycloak_base64url(string $bytes): string
{
    return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}

function qdb_keycloak_unbase64url(string $value): string
{
    $padded = strtr($value, '-_', '+/');
    $padded .= str_repeat('=', (4 - strlen($padded) % 4) % 4);
    $decoded = base64_decode($padded, true);
    if ($decoded === false) {
        throw new InvalidArgumentException('Invalid base64url value');
    }
    return $decoded;
}

function qdb_keycloak_create_state(string $nonce): string
{
    $payload = json_encode([
        'nonce' => $nonce,
        'exp' => time() + 600,
    ], JSON_THROW_ON_ERROR);
    $body = qdb_keycloak_base64url($payload);
    $sig = qdb_keycloak_base64url(hash_hmac('sha256', $body, qdb_keycloak_state_signing_key(), true));
    return $body . '.' . $sig;
}

function qdb_keycloak_verify_state(string $state): array
{
    $parts = explode('.', $state, 2);
    if (count($parts) !== 2) {
        json_error('INVALID_STATE', 'Invalid Keycloak state', 400);
    }
    [$body, $sig] = $parts;
    $expected = qdb_keycloak_base64url(hash_hmac('sha256', $body, qdb_keycloak_state_signing_key(), true));
    if (!hash_equals($expected, $sig)) {
        json_error('INVALID_STATE', 'Invalid Keycloak state', 400);
    }
    $payload = json_decode(qdb_keycloak_unbase64url($body), true);
    if (!is_array($payload) || (int)($payload['exp'] ?? 0) < time()) {
        json_error('INVALID_STATE', 'Expired Keycloak state', 400);
    }
    return $payload;
}

function qdb_keycloak_discovery(array $config): array
{
    $url = $config['issuer'] . '/.well-known/openid-configuration';
    $json = qdb_keycloak_http_json($url);
    foreach (['authorization_endpoint', 'token_endpoint', 'userinfo_endpoint'] as $key) {
        if (empty($json[$key])) {
            throw new RuntimeException("Keycloak discovery missing $key");
        }
    }
    return $json;
}

function qdb_keycloak_http_json(string $url, array $options = []): array
{
    $context = stream_context_create(['http' => [
        'method' => $options['method'] ?? 'GET',
        'header' => $options['headers'] ?? '',
        'content' => $options['content'] ?? '',
        'timeout' => 10,
        'ignore_errors' => true,
    ]]);
    $raw = file_get_contents($url, false, $context);
    if ($raw === false) {
        throw new RuntimeException('Keycloak request failed');
    }
    $status = 200;
    foreach (($http_response_header ?? []) as $header) {
        if (preg_match('#^HTTP/\S+\s+(\d+)#', $header, $m)) {
            $status = (int)$m[1];
            break;
        }
    }
    $json = json_decode($raw, true);
    if ($status < 200 || $status >= 300 || !is_array($json)) {
        throw new RuntimeException('Keycloak returned an invalid response');
    }
    return $json;
}

function qdb_keycloak_exchange_code(array $config, array $discovery, string $code): array
{
    $body = http_build_query([
        'grant_type' => 'authorization_code',
        'code' => $code,
        'redirect_uri' => $config['redirect_uri'],
        'client_id' => $config['client_id'],
        'client_secret' => $config['client_secret'],
    ]);
    return qdb_keycloak_http_json($discovery['token_endpoint'], [
        'method' => 'POST',
        'headers' => "Content-Type: application/x-www-form-urlencoded\r\n",
        'content' => $body,
    ]);
}

function qdb_keycloak_userinfo(array $discovery, string $accessToken): array
{
    return qdb_keycloak_http_json($discovery['userinfo_endpoint'], [
        'headers' => "Authorization: Bearer $accessToken\r\n",
    ]);
}

function qdb_keycloak_claim_username(array $config, array $claims): string
{
    $claim = $config['username_claim'] ?: 'preferred_username';
    $username = trim((string)($claims[$claim] ?? ''));
    if ($username === '' && $claim !== 'email') {
        $username = trim((string)($claims['email'] ?? ''));
    }
    if ($username === '') {
        json_error('KEYCLOAK_NO_USERNAME', 'University login did not provide a usable username', 403);
    }
    return $username;
}

function qdb_keycloak_redirect(string $url, int $status = 302): never
{
    if (defined('QDB_TESTING')) {
        throw new \Tests\Support\RawHttpResponse('', ['Location' => $url], $status);
    }
    http_response_code($status);
    header('Location: ' . $url, true, $status);
    exit;
}

function qdb_keycloak_finish_html(string $token, string $user, string $role): never
{
    $target = qdb_keycloak_frontend_url() . '#/';
    $payload = json_encode([
        'token' => $token,
        'user' => $user,
        'role' => $role,
        'target' => $target,
    ], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_THROW_ON_ERROR);
    qdb_http_download(
        "<!doctype html><meta charset=\"utf-8\"><script>const d=$payload;localStorage.setItem('qdb_token',d.token);localStorage.setItem('qdb_user',d.user);localStorage.setItem('qdb_role',d.role);location.replace(d.target);</script>",
        ['Content-Type' => 'text/html; charset=UTF-8']
    );
}

