<?php
require_once __DIR__ . '/auth_station_bridge.php';

function app_set_runtime_config(array $cfg): void
{
    $GLOBALS['__ly_runtime_config'] = $cfg;
}

function app_read_config_array(string $path): array
{
    if ($path === '' || !is_file($path)) {
        return [];
    }

    $loaded = require $path;
    return is_array($loaded) ? $loaded : [];
}

function app_read_json_env_array(string $envName): array
{
    $raw = trim((string)(getenv($envName) ?: ''));
    if ($raw === '') {
        return [];
    }

    $decoded = json_decode($raw, true);
    return is_array($decoded) ? $decoded : [];
}

function app_merge_config_layers(array ...$layers): array
{
    $merged = [];
    foreach ($layers as $layer) {
        if (is_array($layer)) {
            $merged = array_replace_recursive($merged, $layer);
        }
    }
    return $merged;
}

function app_default_session_name(): string
{
    return 'lyapp_free_session';
}

function app_normalize_app_config(array $cfg): array
{
    $sessionName = trim((string)($cfg['session_name'] ?? ''));
    if ($sessionName === '' || $sessionName === 'lyapp_session') {
        // Compatibility note: keep legacy deployments working, but split free/paid
        // session names when the shared default is still in use.
        $cfg['session_name'] = 'lyapp_free_session';
    }

    $sameSite = strtolower(trim((string)($cfg['session_cookie_samesite'] ?? '')));
    if (!in_array($sameSite, ['strict', 'lax', 'none'], true)) {
        $cfg['session_cookie_samesite'] = 'Strict';
    }

    return $cfg;
}

function app_config_local_file(): string
{
    return __DIR__ . '/../config/config.local.php';
}

function app_config_env_overrides(): array
{
    $overrides = app_read_json_env_array('LY_APP_CONFIG_JSON');
    $map = [
        'LY_APP_NAME' => 'app_name',
        'LY_BASE_URL' => 'base_url',
        'LY_ENTRY_SCRIPT' => 'entry_script',
        'LY_ENTRY_MODE' => 'entry_mode',
        'LY_ENTRY_ROUTE_KEY' => 'entry_route_key',
        'LY_TIMEZONE' => 'timezone',
        'LY_SESSION_NAME' => 'session_name',
        'LY_SESSION_COOKIE_SAMESITE' => 'session_cookie_samesite',
        'LY_TOKEN_SALT' => 'token_salt',
        'LY_API_SECRET' => 'api_secret',
        'LY_AUTH_STATION_URL' => 'auth_station_url',
        'LY_AUTH_STATION_BASE_URL' => 'auth_station_base_url',
        'LY_AUTH_STATION_API_URL' => 'auth_station_api_url',
        'LY_UPDATE_SERVER' => 'update_server',
        'LY_UPDATE_SERVER_URL' => 'update_server_url',
        'LY_UPDATE_API_URL' => 'update_api_url',
        'LY_UPDATE_REQUIRE_SIGNATURE' => 'update_require_signature',
        'LY_APP_VERSION' => 'app_version',
    ];

    foreach ($map as $env => $key) {
        $value = getenv($env);
        if ($value === false || $value === null || $value === '') {
            continue;
        }
        if ($key === 'update_require_signature') {
            $overrides[$key] = filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
            continue;
        }
        $overrides[$key] = (string)$value;
    }

    return $overrides;
}

function app_config(): array {
    static $cfg = null;
    if (!is_array($cfg)) {
        $base = app_read_config_array(__DIR__ . '/../config/config.php');
        $local = app_read_config_array(app_config_local_file());
        $runtime = $GLOBALS['__ly_runtime_config'] ?? null;
        $env = app_config_env_overrides();
        $cfg = app_normalize_app_config(app_merge_config_layers($base, $local, is_array($runtime) ? $runtime : [], $env));
    }

    return $cfg;
}

function app_write_config(array $cfg): bool
{
    $configPath = __DIR__ . '/../config/config.php';
    $tmpPath = $configPath . '.tmp_' . bin2hex(random_bytes(4));
    $content = "<?php\nreturn " . var_export($cfg, true) . ";\n";

    if (@file_put_contents($tmpPath, $content, LOCK_EX) === false) {
        @unlink($tmpPath);
        return false;
    }

    if (!@rename($tmpPath, $configPath)) {
        @unlink($tmpPath);
        return false;
    }

    app_set_runtime_config($cfg);
    return true;
}

function app_security_local_file(): string
{
    return __DIR__ . '/../config/security.local.php';
}

function app_security_config_env_overrides(): array
{
    $overrides = app_read_json_env_array('LY_SECURITY_CONFIG_JSON');
    $map = [
        'LY_AUTH_MODE' => ['auth', 'mode'],
        'LY_AUTH_ISSUER' => ['auth', 'issuer'],
        'LY_AUTH_AUDIENCE' => ['auth', 'audience'],
        'LY_AUTH_API_BASE_URL' => ['auth', 'api_base_url'],
        'LY_AUTH_ALLOWED_HOSTS' => ['auth', 'allowed_hosts'],
        'LY_AUTH_PUBLIC_KEY_PEM' => ['auth', 'public_key_pem'],
        'LY_CSP_MODE' => ['headers', 'csp_mode'],
        'LY_CSP_ALLOW_HTTP' => ['headers', 'csp_allow_http_sources'],
        'LY_CSP_ALLOW_INLINE' => ['headers', 'csp_allow_unsafe_inline'],
        'LY_CSP_ALLOW_EVAL' => ['headers', 'csp_allow_unsafe_eval'],
        'LY_CSP' => ['headers', 'csp'],
        'LY_OFFLINE_GRACE_SECONDS' => ['local', 'offline_grace_seconds'],
        'LY_UPDATE_CONNECT_TIMEOUT' => ['update', 'connect_timeout'],
        'LY_UPDATE_TIMEOUT' => ['update', 'timeout'],
        'LY_UPDATE_RETRIES' => ['update', 'retries'],
        'LY_UPDATE_REQUIRE_SIGNATURE' => ['update', 'require_signature'],
        'LY_UPDATE_ROLLBACK_KEEP' => ['update', 'rollback_keep'],
        'LY_API_TIMESTAMP_WINDOW' => ['api', 'timestamp_window'],
        'LY_API_NONCE_TTL' => ['api', 'nonce_ttl'],
    ];

    foreach ($map as $env => $path) {
        $value = getenv($env);
        if ($value === false || $value === null || $value === '') {
            continue;
        }

        if (!isset($overrides[$path[0]]) || !is_array($overrides[$path[0]])) {
            $overrides[$path[0]] = [];
        }

        if (in_array($path[1], ['connect_timeout', 'timeout', 'retries', 'rollback_keep', 'timestamp_window', 'nonce_ttl', 'offline_grace_seconds'], true)) {
            $overrides[$path[0]][$path[1]] = (int)$value;
        } elseif (in_array($path[1], ['require_signature', 'allow_http_sources', 'allow_unsafe_inline', 'allow_unsafe_eval'], true)) {
            $overrides[$path[0]][$path[1]] = filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
        } elseif ($path[1] === 'allowed_hosts') {
            $overrides[$path[0]][$path[1]] = array_values(array_filter(array_map('trim', explode(',', $value))));
        } else {
            $overrides[$path[0]][$path[1]] = (string)$value;
        }
    }

    return $overrides;
}

function app_normalize_security_config(array $cfg): array
{
    $cfg['auth'] = is_array($cfg['auth'] ?? null) ? $cfg['auth'] : [];
    $cfg['local'] = is_array($cfg['local'] ?? null) ? $cfg['local'] : [];
    $cfg['update'] = is_array($cfg['update'] ?? null) ? $cfg['update'] : [];
    $cfg['api'] = is_array($cfg['api'] ?? null) ? $cfg['api'] : [];
    $cfg['headers'] = is_array($cfg['headers'] ?? null) ? $cfg['headers'] : [];

    if (trim((string)($cfg['auth']['public_key_pem'] ?? '')) === '' && function_exists('app_log_security_event')) {
        static $loggedMissingPublicKey = false;
        if (!$loggedMissingPublicKey) {
            app_log_security_event('missing_public_key_pem', [
                'scope' => 'frontend_security_config',
            ]);
            $loggedMissingPublicKey = true;
        }
    }

    if (!isset($cfg['headers']['csp_mode']) || trim((string)$cfg['headers']['csp_mode']) === '') {
        $cfg['headers']['csp_mode'] = 'compat';
    }
    if (!array_key_exists('csp_allow_http_sources', $cfg['headers'])) {
        $cfg['headers']['csp_allow_http_sources'] = false;
    }
    if (!array_key_exists('csp_allow_unsafe_inline', $cfg['headers'])) {
        $cfg['headers']['csp_allow_unsafe_inline'] = true;
    }
    if (!array_key_exists('csp_allow_unsafe_eval', $cfg['headers'])) {
        $cfg['headers']['csp_allow_unsafe_eval'] = true;
    }

    return $cfg;
}

function app_security_config(): array
{
    static $cfg = null;
    if (!is_array($cfg)) {
        $base = app_read_config_array(__DIR__ . '/../config/security.php');
        $local = app_read_config_array(app_security_local_file());
        $runtime = $GLOBALS['__ly_runtime_security_config'] ?? null;
        $env = app_security_config_env_overrides();
        $cfg = app_normalize_security_config(app_merge_config_layers($base, $local, is_array($runtime) ? $runtime : [], $env));
    }

    return $cfg;
}

function app_set_runtime_security_config(array $cfg): void
{
    $GLOBALS['__ly_runtime_security_config'] = $cfg;
}

function app_write_security_config(array $cfg): bool
{
    $configPath = __DIR__ . '/../config/security.php';
    $tmpPath = $configPath . '.tmp_' . bin2hex(random_bytes(4));
    $content = "<?php\ndeclare(strict_types=1);\nreturn " . var_export($cfg, true) . ";\n";

    if (@file_put_contents($tmpPath, $content, LOCK_EX) === false) {
        @unlink($tmpPath);
        return false;
    }

    if (!@rename($tmpPath, $configPath)) {
        @unlink($tmpPath);
        return false;
    }

    app_set_runtime_security_config($cfg);
    return true;
}

function app_strip_csp_token(string $policy, string $token): string
{
    $policy = trim($policy);
    $token = trim($token);
    if ($policy === '' || $token === '') {
        return $policy;
    }

    $policy = str_replace(" {$token}", '', $policy);
    $policy = str_replace("{$token} ", '', $policy);
    $policy = str_replace($token, '', $policy);
    return trim((string)preg_replace('/\s+/', ' ', $policy));
}

function app_build_csp_policy(array $headers): string
{
    $policy = trim((string)($headers['csp'] ?? ''));
    if ($policy === '') {
        // Compatibility-first default: preserve inline script/style support so old
        // templates continue to render, while still allowing stricter config later.
        $policy = "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; img-src 'self' data: blob: https: http:; connect-src 'self' https: http:; style-src 'self' 'unsafe-inline' https: http:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:; font-src 'self' data: https: http:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';";
    }

    $mode = strtolower(trim((string)($headers['csp_mode'] ?? 'compat')));
    $allowHttp = $mode !== 'strict' && !empty($headers['csp_allow_http_sources']);
    $allowUnsafeInline = array_key_exists('csp_allow_unsafe_inline', $headers) ? !empty($headers['csp_allow_unsafe_inline']) : true;
    $allowUnsafeEval = $mode !== 'strict'
        ? (array_key_exists('csp_allow_unsafe_eval', $headers) ? !empty($headers['csp_allow_unsafe_eval']) : true)
        : false;

    if (!$allowHttp) {
        $policy = app_strip_csp_token($policy, 'http:');
    }
    if (!$allowUnsafeInline) {
        $policy = app_strip_csp_token($policy, "'unsafe-inline'");
    }
    if (!$allowUnsafeEval) {
        $policy = app_strip_csp_token($policy, "'unsafe-eval'");
    }

    return trim((string)preg_replace('/\s+/', ' ', $policy));
}


function app_render_view_scope(string $file, array $data): void
{
    $view = (string)($data['view'] ?? '');
    $layout = (string)($data['layout'] ?? 'main');
    $scope = $data;
    unset($scope['view'], $scope['layout']);

    // Compatibility note: keep legacy template variable names available, but avoid extract()
    // so unexpected payload keys cannot overwrite local symbols.
    foreach ($scope as $__key => $__value) {
        if (!is_string($__key)) {
            continue;
        }
        if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $__key)) {
            continue;
        }
        if (in_array($__key, ['GLOBALS', '_SERVER', '_GET', '_POST', '_COOKIE', '_REQUEST', '_FILES', '_ENV', '_SESSION', 'view', 'layout', 'data'], true)) {
            continue;
        }
        ${$__key} = $__value;
    }

    $data = $scope;
    require $file;
}

function app_json_is_list(array $value): bool
{
    return array_keys($value) === range(0, count($value) - 1);
}

function app_canonicalize_json_value(mixed $value): mixed
{
    if (!is_array($value)) {
        return $value;
    }
    if (app_json_is_list($value)) {
        return array_map(static fn($item) => app_canonicalize_json_value($item), $value);
    }

    ksort($value, SORT_STRING);
    foreach ($value as $key => $item) {
        $value[$key] = app_canonicalize_json_value($item);
    }
    return $value;
}

function app_canonical_json(array $payload): string
{
    return json_encode(
        app_canonicalize_json_value($payload),
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
    );
}

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

function app_b64url_decode(string $text): string
{
    $pad = strlen($text) % 4;
    if ($pad > 0) {
        $text .= str_repeat('=', 4 - $pad);
    }

    $decoded = base64_decode(strtr($text, '-_', '+/'), true);
    return is_string($decoded) ? $decoded : '';
}

function app_generate_secret(int $bytes = 32): string
{
    try {
        return bin2hex(random_bytes(max(16, $bytes)));
    } catch (Throwable $e) {
        return hash('sha256', microtime(true) . '|' . __FILE__ . '|' . mt_rand());
    }
}

function app_derived_secret(string $scope): string
{
    $db = db_config();
    $parts = [
        $scope,
        (string)($db['host'] ?? ''),
        (string)($db['port'] ?? ''),
        (string)($db['database'] ?? ''),
        (string)($db['username'] ?? ''),
        (string)($db['password'] ?? ''),
        (string)php_uname('n'),
        __DIR__,
    ];

    return hash('sha256', implode('|', $parts));
}

function app_ensure_security_runtime(): void
{
    static $done = false;
    if ($done) {
        return;
    }
    $done = true;

    $cfg = app_config();
    if (!is_array($cfg)) {
        return;
    }

    $changed = false;

    $tokenSalt = trim((string)($cfg['token_salt'] ?? ''));
    $tokenSaltMissing = ($tokenSalt === '' || $tokenSalt === 'change_this_to_a_random_string');
    if ($tokenSaltMissing) {
        $cfg['token_salt'] = app_generate_secret(32);
        $changed = true;
    }

    $apiSecret = trim((string)($cfg['api_secret'] ?? ''));
    $apiSecretMissing = ($apiSecret === '' || $apiSecret === 'change_this_to_a_random_api_secret');
    if ($apiSecretMissing) {
        $cfg['api_secret'] = app_generate_secret(32);
        $changed = true;
    }

    if (!isset($cfg['session_cookie_samesite']) || trim((string)$cfg['session_cookie_samesite']) === '') {
        $cfg['session_cookie_samesite'] = 'Strict';
        $changed = true;
    }

    if (!array_key_exists('update_require_signature', $cfg)) {
        $cfg['update_require_signature'] = false;
        $changed = true;
    }

    $normalizedUpdateApi = station_normalize_update_api_url((string)($cfg['update_api_url'] ?? ''));
    if ($normalizedUpdateApi !== '' && !hash_equals($normalizedUpdateApi, (string)($cfg['update_api_url'] ?? ''))) {
        $cfg['update_api_url'] = $normalizedUpdateApi;
        $changed = true;
    }

    if ($changed) {
        if (!app_write_config($cfg)) {
            if ($tokenSaltMissing) {
                $cfg['token_salt'] = app_derived_secret('token_salt');
            }
            if ($apiSecretMissing) {
                $cfg['api_secret'] = app_derived_secret('api_secret');
            }
            app_set_runtime_config($cfg);
        }
        return;
    }

    app_set_runtime_config($cfg);
}

function app_ensure_bridge_security_runtime(): void
{
    static $done = false;
    if ($done) {
        return;
    }
    $done = true;

    $cfg = app_security_config();
    if (!is_array($cfg)) {
        $cfg = [];
    }

    $changed = false;
    if (!isset($cfg['auth']) || !is_array($cfg['auth'])) {
        $cfg['auth'] = [];
        $changed = true;
    }
    if (!isset($cfg['local']) || !is_array($cfg['local'])) {
        $cfg['local'] = [];
        $changed = true;
    }
    if (!isset($cfg['update']) || !is_array($cfg['update'])) {
        $cfg['update'] = [];
        $changed = true;
    }
    if (!isset($cfg['api']) || !is_array($cfg['api'])) {
        $cfg['api'] = [];
        $changed = true;
    }
    if (!isset($cfg['rate_limit']) || !is_array($cfg['rate_limit'])) {
        $cfg['rate_limit'] = [];
        $changed = true;
    }
    if (!isset($cfg['headers']) || !is_array($cfg['headers'])) {
        $cfg['headers'] = [];
        $changed = true;
    }

    if (trim((string)($cfg['local']['install_id'] ?? '')) === '') {
        $cfg['local']['install_id'] = 'inst_' . app_generate_secret(16);
        $changed = true;
    }
    if (trim((string)($cfg['local']['cache_key'] ?? '')) === '') {
        $cfg['local']['cache_key'] = app_generate_secret(32);
        $changed = true;
    }
    if ((int)($cfg['local']['offline_grace_seconds'] ?? 0) < 300) {
        $cfg['local']['offline_grace_seconds'] = 86400;
        $changed = true;
    }
    if ((int)($cfg['update']['connect_timeout'] ?? 0) < 3) {
        $cfg['update']['connect_timeout'] = 5;
        $changed = true;
    }
    if ((int)($cfg['update']['timeout'] ?? 0) < 6) {
        $cfg['update']['timeout'] = 12;
        $changed = true;
    }
    if (!array_key_exists('retries', $cfg['update']) || (int)($cfg['update']['retries'] ?? 0) < 2) {
        $cfg['update']['retries'] = 2;
        $changed = true;
    }
    if (!array_key_exists('require_signature', $cfg['update'])) {
        $cfg['update']['require_signature'] = false;
        $changed = true;
    }
    if ((int)($cfg['update']['rollback_keep'] ?? 0) < 1) {
        $cfg['update']['rollback_keep'] = 3;
        $changed = true;
    }
    if ((int)($cfg['api']['timestamp_window'] ?? 0) < 60) {
        $cfg['api']['timestamp_window'] = 300;
        $changed = true;
    }
    if ((int)($cfg['api']['nonce_ttl'] ?? 0) < 60) {
        $cfg['api']['nonce_ttl'] = 300;
        $changed = true;
    }
    if (!isset($cfg['api']['keys']) || !is_array($cfg['api']['keys'])) {
        $cfg['api']['keys'] = [];
        $changed = true;
    }
    if (!isset($cfg['rate_limit']['driver']) || trim((string)$cfg['rate_limit']['driver']) === '') {
        $cfg['rate_limit']['driver'] = 'auto';
        $changed = true;
    }
    if (!isset($cfg['rate_limit']['redis']) || !is_array($cfg['rate_limit']['redis'])) {
        $cfg['rate_limit']['redis'] = [
            'host' => '127.0.0.1',
            'port' => 6379,
            'password' => '',
            'database' => 0,
            'prefix' => 'ly:ratelimit:',
            'timeout' => 1.5,
        ];
        $changed = true;
    }
    if (!isset($cfg['headers']['csp']) || trim((string)$cfg['headers']['csp']) === '') {
        $cfg['headers']['csp'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; img-src 'self' data: blob: https: http:; connect-src 'self' https: http:; style-src 'self' 'unsafe-inline' https: http:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:; font-src 'self' data: https: http:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';";
        $changed = true;
    }
    if (!isset($cfg['headers']['x_xss_protection']) || trim((string)$cfg['headers']['x_xss_protection']) === '') {
        $cfg['headers']['x_xss_protection'] = '1; mode=block';
        $changed = true;
    }

    if ($changed) {
        if (!app_write_security_config($cfg)) {
            app_set_runtime_security_config($cfg);
        }
        return;
    }

    app_set_runtime_security_config($cfg);
}

function db_config(): array {
    static $cfg;
    if (!$cfg) {
        $cfg = require __DIR__ . '/../config/database.php';
    }
    return $cfg;
}

function base_url(string $path = ''): string {
    $cfg = app_config();
    $base = rtrim($cfg['base_url'] ?? '/', '/');
    $entryScript = trim((string)($cfg['entry_script'] ?? ''));
    $entryMode = trim((string)($cfg['entry_mode'] ?? 'path'));
    $routeKey = trim((string)($cfg['entry_route_key'] ?? 'r'));
    $path = ltrim($path, '/');

    if ($path === '') {
        return ($base === '' ? '' : $base) . '/';
    }

    if ($entryScript !== '') {
        $entry = '/' . trim($entryScript, '/');
        if ($entryMode === 'query') {
            // Encode each segment to avoid path confusion when rebuilding proxy-aware URLs.
            $segments = array_map('rawurlencode', array_filter(explode('/', $path), 'strlen'));
            $encodedPath = implode('/', $segments);
            return ($base === '' ? '' : $base) . $entry . '?' . rawurlencode($routeKey) . '=' . $encodedPath;
        }
        return ($base === '' ? '' : $base) . $entry . '/' . $path;
    }

    return ($base === '' ? '' : $base) . '/' . $path;
}

function app_static_base_url(): string
{
    $cfg = app_config();
    $base = trim((string)($cfg['base_url'] ?? '/'));
    if ($base === '' || $base === '/') {
        return '';
    }
    return rtrim($base, '/');
}

function app_detect_public_prefix(): string
{
    static $prefix = null;
    if (is_string($prefix)) {
        return $prefix;
    }

    $docRoot = rtrim(str_replace('\\', '/', (string)($_SERVER['DOCUMENT_ROOT'] ?? '')), '/');
    if ($docRoot !== '') {
        if (is_dir($docRoot . '/assets/lyear') || is_file($docRoot . '/favicon.ico')) {
            $prefix = '';
            return $prefix;
        }
        if (is_dir($docRoot . '/public/assets/lyear') || is_file($docRoot . '/public/favicon.ico')) {
            $prefix = 'public';
            return $prefix;
        }
    }

    $requestPath = (string)parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH);
    if ($requestPath === '/public' || strpos($requestPath, '/public/') === 0) {
        $prefix = 'public';
        return $prefix;
    }

    $prefix = '';
    return $prefix;
}

function app_uses_public_root(): bool
{
    return app_detect_public_prefix() === '';
}

function app_pretty_url(string $path = ''): string
{
    $path = ltrim($path, '/');
    $base = app_static_base_url();
    $relative = $path === '' ? '/' : (($base === '' ? '' : $base) . '/' . $path);
    if ($relative === '') {
        $relative = '/';
    }

    $host = app_request_host();
    if ($host === '') {
        return $relative;
    }

    $scheme = app_forwarded_proto_hint();
    if ($scheme !== 'https' && $scheme !== 'http') {
        $scheme = app_request_is_https() ? 'https' : 'http';
    }

    return $scheme . '://' . $host . $relative;
}

function public_file_url(string $path = ''): string
{
    $base = app_static_base_url();
    $path = trim($path, '/');
    $prefix = app_detect_public_prefix();

    $target = $path;
    if ($prefix !== '') {
        $target = $target === '' ? $prefix : ($prefix . '/' . $target);
    }

    if ($base === '') {
        return $target === '' ? '/' : ('/' . ltrim($target, '/'));
    }
    if ($target === '') {
        return rtrim($base, '/') . '/';
    }
    return rtrim($base, '/') . '/' . ltrim($target, '/');
}

function asset_url(string $path = ''): string
{
    $path = ltrim($path, '/');
    $target = 'assets/lyear';
    if ($path !== '') {
        $target .= '/' . $path;
    }
    return public_file_url($target);
}

function by_asset_url(string $path = ''): string
{
    $path = trim(str_replace('\\', '/', $path), '/');
    $url = public_file_url('by_asset.php');
    if ($path === '') {
        return $url;
    }
    return $url . '?path=' . rawurlencode($path);
}

function by_bundle_assets(): array
{
    static $assets = null;
    if (is_array($assets)) {
        return $assets;
    }

    $root = dirname(__DIR__) . '/by';
    $assets = [
        'css' => '',
        'js' => '',
        'favicon' => '',
    ];

    if (!is_dir($root)) {
        return $assets;
    }

    $cssFiles = glob($root . '/css/*.css') ?: [];
    sort($cssFiles, SORT_NATURAL);
    if (!empty($cssFiles)) {
        $assets['css'] = 'css/' . basename((string)$cssFiles[0]);
    }

    $jsFiles = glob($root . '/js/*.js') ?: [];
    sort($jsFiles, SORT_NATURAL);
    if (!empty($jsFiles)) {
        $assets['js'] = 'js/' . basename((string)$jsFiles[0]);
    }

    if (is_file($root . '/favicon.svg')) {
        $assets['favicon'] = 'favicon.svg';
    }

    return $assets;
}

function station_default_update_server(): string
{
    return '';
}

function station_normalize_update_api_url(string $url): string
{
    $url = trim($url);
    if ($url === '' || preg_match('/[\s,|]+/', $url)) {
        return '';
    }

    if (!preg_match('#^https?://#i', $url)) {
        $url = 'https://' . ltrim($url, '/');
    }

    $parsed = parse_url($url);
    $host = is_array($parsed) ? (string)($parsed['host'] ?? '') : '';
    if (!is_string($host) || trim($host) === '') {
        return '';
    }

    $path = is_array($parsed) ? (string)($parsed['path'] ?? '') : '';
    $query = is_array($parsed) ? (string)($parsed['query'] ?? '') : '';
    $scheme = is_array($parsed) ? strtolower((string)($parsed['scheme'] ?? '')) : '';
    $port = is_array($parsed) && array_key_exists('port', $parsed) ? (int)$parsed['port'] : 0;

    if ($path === '' || $path === '/' || $path === '\\') {
        $url = ($scheme !== '' ? $scheme : 'https') . '://' . $host;
        if ($port > 0) {
            $url .= ':' . $port;
        }
        if ($query !== '') {
            $url .= '?' . $query;
        }
    }

    return preg_match('#^https?://#i', $url) ? $url : '';
}

function station_update_server(): string
{
    $cfg = app_config();
    $configured = station_normalize_update_api_url((string)($cfg['update_api_url'] ?? ''));
    return $configured !== '' ? $configured : '';
}

function e($value): string {
    return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function e_attr($value): string
{
    return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function e_url($value): string
{
    $value = trim((string)$value);
    if ($value === '') {
        return '';
    }

    return e_attr($value);
}

function app_json_for_html(mixed $value): string
{
    try {
        $json = json_encode(
            $value,
            JSON_UNESCAPED_UNICODE
            | JSON_UNESCAPED_SLASHES
            | JSON_HEX_TAG
            | JSON_HEX_AMP
            | JSON_HEX_APOS
            | JSON_HEX_QUOT
            | JSON_THROW_ON_ERROR
        );
    } catch (Throwable $e) {
        $json = 'null';
    }

    return is_string($json) ? $json : 'null';
}

function app_mask_sensitive_text(string $text): string
{
    $text = (string)$text;
    $text = (string)preg_replace('#https?://[^\s<>"\'()]+#i', '[hidden-link]', $text);
    $text = (string)preg_replace('/\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b/i', '[hidden-domain]', $text);
    return $text;
}

function app_safe_mkdir(string $dir, int $mode = 0755): bool
{
    $dir = trim($dir);
    if ($dir === '') {
        return false;
    }

    if (!is_dir($dir) && !@mkdir($dir, $mode, true) && !is_dir($dir)) {
        return false;
    }

    if (is_dir($dir)) {
        @chmod($dir, $mode);
    }

    return is_dir($dir);
}

function app_protect_public_upload_directory(string $dir): void
{
    $dir = trim($dir);
    if ($dir === '' || !is_dir($dir)) {
        return;
    }

    // Compatibility note: this only adds passive guard files; it does not change how
    // existing uploads are served, so old deployments keep working as-is.
    $guards = [
        $dir . DIRECTORY_SEPARATOR . 'index.html' => "<!doctype html><meta charset=\"utf-8\"><title>403 Forbidden</title>",
        $dir . DIRECTORY_SEPARATOR . '.htaccess' => "Options -Indexes\n<FilesMatch \"\\.(php|phtml|phar|cgi|pl|py|jsp|asp|aspx)$\">\n    Require all denied\n</FilesMatch>\n",
    ];

    foreach ($guards as $path => $content) {
        if (is_file($path)) {
            continue;
        }
        @file_put_contents($path, $content, LOCK_EX);
    }
}

function app_log_security_event(string $event, array $context = []): void
{
    foreach (['token', 'authorization', 'secret', 'private_key', 'password', 'ticket'] as $key) {
        if (isset($context[$key])) {
            $context[$key] = '[redacted]';
        }
    }

    $line = json_encode([
        'ts' => date(DATE_ATOM),
        'request_id' => (string)($_SERVER['APP_REQUEST_ID'] ?? ''),
        'event' => $event,
        'context' => $context,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if (!is_string($line)) {
        return;
    }

    $file = __DIR__ . '/../storage/logs/security.log';
    app_safe_mkdir(dirname($file), 0755);
    @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
}

function app_log_audit_event(string $event, array $context = []): void
{
    foreach (['token', 'authorization', 'secret', 'private_key', 'password'] as $key) {
        if (isset($context[$key])) {
            $context[$key] = '[redacted]';
        }
    }

    $line = json_encode([
        'ts' => date(DATE_ATOM),
        'request_id' => (string)($_SERVER['APP_REQUEST_ID'] ?? ''),
        'event' => $event,
        'ip' => function_exists('app_client_ip') ? app_client_ip() : '',
        'user_id' => (int)($_SESSION['user_id'] ?? 0),
        'context' => $context,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if (!is_string($line)) {
        return;
    }

    $file = __DIR__ . '/../storage/logs/audit.log';
    app_safe_mkdir(dirname($file), 0755);
    @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
}

function app_log_api_event(string $event, array $context = []): void
{
    foreach (['token', 'authorization', 'secret', 'private_key', 'password'] as $key) {
        if (isset($context[$key])) {
            $context[$key] = '[redacted]';
        }
    }

    $line = json_encode([
        'ts' => date(DATE_ATOM),
        'request_id' => (string)($_SERVER['APP_REQUEST_ID'] ?? ''),
        'event' => $event,
        'ip' => function_exists('app_client_ip') ? app_client_ip() : '',
        'method' => (string)($_SERVER['REQUEST_METHOD'] ?? 'GET'),
        'uri' => (string)($_SERVER['REQUEST_URI'] ?? ''),
        'context' => $context,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if (!is_string($line)) {
        return;
    }

    $file = __DIR__ . '/../storage/logs/api.log';
    app_safe_mkdir(dirname($file), 0755);
    @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
}

function app_send_security_headers(): void
{
    if (headers_sent()) {
        return;
    }

    app_ensure_bridge_security_runtime();
    $cfg = app_security_config();
    $headers = is_array($cfg['headers'] ?? null) ? (array)$cfg['headers'] : [];

    header('X-Content-Type-Options: nosniff');
    header('X-Frame-Options: DENY');
    header('Referrer-Policy: strict-origin-when-cross-origin');
    header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
    header('X-XSS-Protection: ' . (string)($headers['x_xss_protection'] ?? '1; mode=block'));
    header('Origin-Agent-Cluster: ?1');

    $csp = app_build_csp_policy($headers);
    if ($csp !== '') {
        header('Content-Security-Policy: ' . $csp);
    }

    if (app_request_is_https()) {
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
    }
}

function app_zh(string $escaped): string
{
    $decoded = json_decode('"' . $escaped . '"');
    return is_string($decoded) ? $decoded : $escaped;
}

function app_clean_text(string $value, int $maxLen = 0): string
{
    $value = app_force_valid_utf8($value);
    $value = trim((string)preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value));
    if ($maxLen > 0 && function_exists('mb_substr')) {
        $value = mb_substr($value, 0, $maxLen, 'UTF-8');
    } elseif ($maxLen > 0) {
        $value = substr($value, 0, $maxLen);
    }
    return $value;
}

function app_force_valid_utf8(string $value): string
{
    if ($value === '') {
        return '';
    }

    if (preg_match('//u', $value) === 1) {
        return $value;
    }

    if (function_exists('iconv')) {
        $clean = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
        if (is_string($clean)) {
            return $clean;
        }
    }

    if (function_exists('mb_convert_encoding')) {
        return mb_convert_encoding($value, 'UTF-8', 'UTF-8');
    }

    return $value;
}

function app_filter_request_key($key): string
{
    $key = app_force_valid_utf8((string)$key);
    $key = (string)preg_replace('/[\x00-\x1F\x7F]/u', '', $key);
    $key = trim($key);
    if ($key === '') {
        return '_';
    }

    if (function_exists('mb_substr')) {
        return mb_substr($key, 0, 128, 'UTF-8');
    }

    return substr($key, 0, 128);
}

function app_filter_request_value($value, int $depth = 0)
{
    if ($depth > 8) {
        return [];
    }

    if (is_array($value)) {
        $clean = [];
        foreach ($value as $k => $v) {
            $clean[app_filter_request_key($k)] = app_filter_request_value($v, $depth + 1);
        }
        return $clean;
    }
    if (!is_string($value)) {
        return $value;
    }

    $value = app_force_valid_utf8($value);
    $value = (string)preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value);
    $value = str_replace("\0", '', $value);
    $value = trim($value);
    if (function_exists('mb_substr')) {
        $value = mb_substr($value, 0, 16384, 'UTF-8');
    } else {
        $value = substr($value, 0, 16384);
    }
    return $value;
}

function app_sanitize_request_globals(): void
{
    if (!empty($GLOBALS['__app_request_globals_sanitized'])) {
        return;
    }

    $_GET = is_array($_GET) ? app_filter_request_value($_GET) : [];
    $_POST = is_array($_POST) ? app_filter_request_value($_POST) : [];
    $_COOKIE = is_array($_COOKIE) ? app_filter_request_value($_COOKIE) : [];
    $_FILES = is_array($_FILES) ? app_filter_request_value($_FILES) : [];
    $_REQUEST = array_merge($_GET, $_POST, $_COOKIE);

    $GLOBALS['__app_request_globals_sanitized'] = true;
}

function app_clean_multiline_text(string $value, int $maxLen = 0): string
{
    $value = str_replace("\r\n", "\n", $value);
    $value = app_force_valid_utf8($value);
    $value = (string)preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value);
    $value = trim($value);
    if ($maxLen > 0 && function_exists('mb_substr')) {
        $value = mb_substr($value, 0, $maxLen, 'UTF-8');
    } elseif ($maxLen > 0) {
        $value = substr($value, 0, $maxLen);
    }
    return $value;
}

function app_request_data(): array
{
    $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
    if ($method === 'GET') {
        return is_array($_GET) ? $_GET : [];
    }

    $ct = (string)($_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '');
    if (stripos($ct, 'application/json') !== false) {
        $raw = file_get_contents('php://input');
        $json = json_decode($raw ?: '', true);
        if (is_array($json)) {
            return app_filter_request_value($json);
        }
        if (($raw ?? '') !== '') {
            app_log_security_event('invalid_json_request_body', [
                'content_type' => app_mask_sensitive_text($ct),
                'body_preview' => app_mask_sensitive_text(substr((string)$raw, 0, 200)),
            ]);
        }
    }

    return is_array($_POST) ? $_POST : [];
}

function app_default_key_prices(): array
{
    return [
        1 => 0,
        5 => 0,
        24 => 0,
        72 => 0,
        168 => 0,
        336 => 0,
        720 => 0,
        1440 => 0,
    ];
}

function app_normalize_key_prices($prices, bool $allowEmpty = false): array
{
    if (!is_array($prices)) {
        return $allowEmpty ? [] : app_default_key_prices();
    }

    $normalized = [];
    foreach ($prices as $hours => $price) {
        $hours = (int)$hours;
        if ($hours < 1) {
            continue;
        }
        if (!is_numeric($price)) {
            continue;
        }
        $normalized[$hours] = (float)$price;
    }

    ksort($normalized, SORT_NUMERIC);
    if ($normalized !== []) {
        return $normalized;
    }

    return $allowEmpty ? [] : app_default_key_prices();
}

function app_parse_key_prices_text(string $text, bool $allowEmpty = false): array
{
    $text = trim($text);
    if ($text === '') {
        if ($allowEmpty) {
            return [];
        }
        return app_default_key_prices();
    }

    $normalized = [];
    $parts = preg_split('/[\r\n,;]+/', $text) ?: [];
    foreach ($parts as $part) {
        $line = trim((string)$part);
        if ($line === '') {
            continue;
        }
        // Keep this message ASCII-safe to avoid encoding corruption causing parse errors on some deployments.
        if (!preg_match('/^(\d+)\s*=\s*([0-9]+(?:\.[0-9]+)?)$/', $line, $m)) {
            throw new InvalidArgumentException('Invalid price format. Use hours=price, for example 24=10');
        }
        $normalized[(int)$m[1]] = (float)$m[2];
    }

    if ($normalized === []) {
        if ($allowEmpty) {
            return [];
        }
        throw new InvalidArgumentException('Price configuration cannot be empty');
    }

    ksort($normalized, SORT_NUMERIC);
    return $normalized;
}

function app_key_prices_to_text(array $prices, string $separator = "\n"): string
{
    $lines = [];
    foreach (app_normalize_key_prices($prices, true) as $hours => $price) {
        $lines[] = $hours . '=' . $price;
    }
    return implode($separator, $lines);
}

function app_default_game_catalog(?array $defaultPrices = null): array
{
    $prices = app_normalize_key_prices($defaultPrices ?? app_default_key_prices());
    return [
        'PUBG' => [
            'code' => 'PUBG',
            'name' => 'PUBG Mobile',
            'prices' => $prices,
        ],
    ];
}

function app_game_catalog(?array $cfg = null): array
{
    $cfg = $cfg ?? app_config();
    $defaultPrices = app_normalize_key_prices($cfg['key_prices'] ?? app_default_key_prices());
    $rawGames = $cfg['games'] ?? [];
    if (!is_array($rawGames) || $rawGames === []) {
        return app_default_game_catalog($defaultPrices);
    }

    $games = [];
    foreach ($rawGames as $key => $row) {
        if (!is_array($row)) {
            $row = ['name' => (string)$row];
        }

        $code = trim((string)($row['code'] ?? (is_string($key) ? $key : '')));
        $code = strtoupper((string)preg_replace('/[^A-Za-z0-9_-]/', '', $code));
        if ($code === '') {
            continue;
        }

        $name = trim((string)($row['name'] ?? $row['label'] ?? $code));
        if ($name === '') {
            $name = $code;
        }

        $prices = app_normalize_key_prices($row['prices'] ?? [], true);
        if ($prices === []) {
            $prices = $defaultPrices;
        }

        $games[$code] = [
            'code' => $code,
            'name' => $name,
            'prices' => $prices,
        ];
    }

    return $games !== [] ? $games : app_default_game_catalog($defaultPrices);
}

function app_game_catalog_to_text(array $games): string
{
    $lines = [];
    foreach ($games as $code => $game) {
        if (!is_array($game)) {
            continue;
        }
        $gameCode = trim((string)($game['code'] ?? $code));
        $gameName = trim((string)($game['name'] ?? ''));
        if ($gameCode === '' || $gameName === '') {
            continue;
        }
        $priceText = app_key_prices_to_text((array)($game['prices'] ?? []), ',');
        $lines[] = $gameCode . '|' . $gameName . '|' . $priceText;
    }
    return implode("\n", $lines);
}

function app_parse_game_catalog_text(string $text, array $defaultPrices = []): array
{
    $text = trim($text);
    if ($text === '') {
        throw new InvalidArgumentException('Game catalog configuration cannot be empty');
    }

    $fallbackPrices = app_normalize_key_prices($defaultPrices ?: app_default_key_prices());
    $lines = preg_split('/\r?\n/', $text) ?: [];
    $games = [];
    foreach ($lines as $index => $rawLine) {
        $line = trim((string)$rawLine);
        if ($line === '') {
            continue;
        }

        $parts = array_map('trim', explode('|', $line));
        if (count($parts) < 2) {
            throw new InvalidArgumentException('Line ' . ($index + 1) . ' format is invalid. Use code|name|prices');
        }

        $code = strtoupper((string)preg_replace('/[^A-Za-z0-9_-]/', '', (string)$parts[0]));
        $name = trim((string)$parts[1]);
        if ($code === '') {
            throw new InvalidArgumentException('Line ' . ($index + 1) . ' game code cannot be empty');
        }
        if ($name === '') {
            throw new InvalidArgumentException('Line ' . ($index + 1) . ' game name cannot be empty');
        }

        $priceText = trim((string)($parts[2] ?? ''));
        $prices = $priceText !== ''
            ? app_parse_key_prices_text($priceText, true)
            : [];
        if ($prices === []) {
            $prices = $fallbackPrices;
        }

        $games[$code] = [
            'code' => $code,
            'name' => $name,
            'prices' => $prices,
        ];
    }

    if ($games === []) {
        throw new InvalidArgumentException('Game catalog configuration cannot be empty');
    }

    return $games;
}

function app_url_with_query(string $url, array $params = []): string
{
    $filtered = [];
    foreach ($params as $key => $value) {
        if ($value === null) {
            continue;
        }
        if (is_string($value)) {
            $value = trim($value);
            if ($value === '') {
                continue;
            }
        }
        $filtered[$key] = $value;
    }

    if ($filtered === []) {
        return $url;
    }

    return $url . (strpos($url, '?') === false ? '?' : '&') . http_build_query($filtered);
}

function app_generate_batch_code(): string
{
    try {
        $suffix = strtoupper(substr(bin2hex(random_bytes(3)), 0, 6));
    } catch (Throwable $e) {
        $suffix = strtoupper(substr(hash('sha256', uniqid('', true)), 0, 6));
    }

    return 'B' . date('YmdHis') . $suffix;
}

function app_absolute_url(string $path = ''): string
{
    $path = base_url($path);
    if (preg_match('#^https?://#i', $path)) {
        return $path;
    }

    $cfg = app_config();
    $base = rtrim((string)($cfg['base_url'] ?? ''), '/');
    if (($base === '' || $base === '/') && $path !== '') {
        $scriptName = (string)($_SERVER['SCRIPT_NAME'] ?? '');
        if ($scriptName !== '' && strpos($scriptName, '/') === 0) {
            $scriptDir = rtrim(str_replace('\\', '/', dirname($scriptName)), '/');
            if ($scriptDir !== '' && $scriptDir !== '.') {
                $path = '/' . ltrim($scriptDir . '/' . ltrim($path, '/'), '/');
            }
        }
    }

    $host = app_request_host();
    if ($host === '') {
        return $path;
    }

    $scheme = '';
    $forwardedProto = app_forwarded_proto_hint();
    if ($forwardedProto !== '') {
        $scheme = $forwardedProto;
    }
    if ($scheme !== 'https' && $scheme !== 'http') {
        $requestScheme = strtolower(trim((string)($_SERVER['REQUEST_SCHEME'] ?? '')));
        if ($requestScheme === 'https' || $requestScheme === 'http') {
            $scheme = $requestScheme;
        }
    }
    if ($scheme !== 'https' && $scheme !== 'http') {
        $scheme = app_request_is_https() ? 'https' : 'http';
    }

    return $scheme . '://' . $host . $path;
}

function redirect(string $path): void {
    $path = str_replace(["\r", "\n"], '', $path);
    if (preg_match('#^https?://#i', $path)) {
        header('Location: ' . $path);
        exit;
    }

    header('Location: ' . base_url($path));
    exit;
}

function view(string $name, array $data = []): void {
    $view = $name;
    $layout = $data['layout'] ?? 'main';
    // Compatibility note: keep existing layout variables, but render in an isolated scope
    // so payload keys cannot pollute local symbols.
    app_render_view_scope(__DIR__ . '/Views/layouts/' . $layout . '.php', array_replace($data, [
        'view' => $view,
        'layout' => $layout,
        'data' => $data,
    ]));
}

function csrf_token(): string {
    if (empty($_SESSION['_csrf'])) {
        $_SESSION['_csrf'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['_csrf'];
}

function csrf_field(): string {
    return '<input type="hidden" name="_csrf" value="' . e(csrf_token()) . '">';
}

function app_request_header(string $name): string
{
    $serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
    return app_clean_text((string)($_SERVER[$serverKey] ?? ''), 2048);
}

function app_same_origin_request(): bool
{
    $requestHost = strtolower(trim((string)app_request_host()));
    if ($requestHost === '') {
        return true;
    }
    $host = strtolower((string)(parse_url((app_request_is_https() ? 'https' : 'http') . '://' . $requestHost, PHP_URL_HOST) ?: $requestHost));

    $origin = app_request_header('Origin');
    $referer = app_request_header('Referer');
    $source = $origin !== '' ? $origin : $referer;
    if ($source !== '') {
        $sourceHost = strtolower((string)parse_url($source, PHP_URL_HOST));
        if ($sourceHost !== '' && !hash_equals($host, $sourceHost)) {
            return false;
        }
    }

    $fetchSite = strtolower(app_request_header('Sec-Fetch-Site'));
    if ($fetchSite !== '' && !in_array($fetchSite, ['same-origin', 'same-site', 'none'], true)) {
        return false;
    }

    return true;
}

function verify_csrf(): bool {
    $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
    if ($method !== 'POST') {
        return false;
    }

    if (!app_same_origin_request()) {
        app_log_security_event('csrf_origin_mismatch', [
            'origin' => app_request_header('Origin'),
            'referer' => app_request_header('Referer'),
        ]);
        return false;
    }

    $payload = app_request_data();
    $token = is_array($payload) ? (string)($payload['_csrf'] ?? '') : '';
    if ($token === '' || strlen($token) < 32) {
        return false;
    }

    $sessionToken = (string)($_SESSION['_csrf'] ?? '');
    return $sessionToken !== '' && hash_equals($sessionToken, $token);
}

function flash(string $key, $value = null) {
    if ($value === null) {
        $val = $_SESSION['_flash'][$key] ?? null;
        unset($_SESSION['_flash'][$key]);
        return $val;
    }
    $_SESSION['_flash'][$key] = $value;
}

function app_login_math_threshold(): int
{
    return 5;
}

function app_login_math_window(): int
{
    return 1800;
}

function app_login_math_counter_keys(string $username = ''): array
{
    $ip = app_client_ip();
    $keys = ['login_math_ip_' . md5($ip)];
    $username = strtolower(app_clean_text($username, 64));
    if ($username !== '') {
        $keys[] = 'login_math_user_' . md5($ip . '|' . $username);
    }
    return $keys;
}

function app_login_math_failure_count(string $username = ''): int
{
    $max = 0;
    foreach (app_login_math_counter_keys($username) as $key) {
        $cached = station_cache_get($key, app_login_math_window());
        $count = (int)($cached['count'] ?? 0);
        if ($count > $max) {
            $max = $count;
        }
    }
    return $max;
}

function app_login_math_increment_failure(string $username = ''): int
{
    $max = 0;
    foreach (app_login_math_counter_keys($username) as $key) {
        $cached = station_cache_get($key, app_login_math_window());
        $count = (int)($cached['count'] ?? 0) + 1;
        station_cache_set($key, ['count' => $count]);
        if ($count > $max) {
            $max = $count;
        }
    }
    return $max;
}

function app_login_math_reset_failures(string $username = ''): void
{
    foreach (app_login_math_counter_keys($username) as $key) {
        station_cache_delete($key);
    }
}

function app_login_math_secret(): string
{
    return app_derived_secret('login_math');
}

function app_login_math_active(string $username = ''): ?array
{
    $challenge = $_SESSION['_login_math'] ?? null;
    if (!is_array($challenge)) {
        return null;
    }

    if ((int)($challenge['expires_at'] ?? 0) < time()) {
        unset($_SESSION['_login_math']);
        return null;
    }

    $ip = app_client_ip();
    if (!hash_equals((string)($challenge['ip'] ?? ''), $ip)) {
        unset($_SESSION['_login_math']);
        return null;
    }

    $storedUser = strtolower((string)($challenge['username'] ?? ''));
    $currentUser = strtolower(app_clean_text($username, 64));
    if ($storedUser !== '' && $currentUser !== '' && !hash_equals($storedUser, $currentUser)) {
        return null;
    }

    return $challenge;
}

function app_login_math_issue(string $username = ''): array
{
    try {
        $a = random_int(2, 19);
        $b = random_int(2, 19);
        $op = random_int(0, 1) === 0 ? '+' : '-';
    } catch (Throwable $e) {
        $a = mt_rand(2, 19);
        $b = mt_rand(2, 19);
        $op = mt_rand(0, 1) === 0 ? '+' : '-';
    }

    if ($op === '-' && $b > $a) {
        [$a, $b] = [$b, $a];
    }

    $answer = ($op === '+') ? ($a + $b) : ($a - $b);
    $challenge = [
        'ip' => app_client_ip(),
        'username' => strtolower(app_clean_text($username, 64)),
        'question' => $a . ' ' . $op . ' ' . $b . ' = ?',
        'answer_hash' => hash_hmac('sha256', (string)$answer, app_login_math_secret()),
        'expires_at' => time() + 900,
    ];
    $_SESSION['_login_math'] = $challenge;
    return $challenge;
}

function app_login_math_required(string $username = ''): bool
{
    if (app_login_math_failure_count($username) >= app_login_math_threshold()) {
        return true;
    }
    return is_array(app_login_math_active($username));
}

function app_login_math_payload(string $username = ''): array
{
    if (!app_login_math_required($username)) {
        unset($_SESSION['_login_math']);
        return ['required' => false, 'question' => '', 'count' => app_login_math_failure_count($username)];
    }

    $active = app_login_math_active($username);
    if (!is_array($active)) {
        $active = app_login_math_issue($username);
    }

    return [
        'required' => true,
        'question' => (string)($active['question'] ?? ''),
        'count' => app_login_math_failure_count($username),
    ];
}

function app_login_math_verify(string $username, string $answer): bool
{
    if (!app_login_math_required($username)) {
        return true;
    }

    $active = app_login_math_active($username);
    if (!is_array($active)) {
        return false;
    }

    $answer = preg_replace('/\s+/', '', $answer);
    if ($answer === '' || !preg_match('/^-?\d+$/', $answer)) {
        return false;
    }

    $expected = (string)($active['answer_hash'] ?? '');
    $actual = hash_hmac('sha256', (string)$answer, app_login_math_secret());
    $ok = hash_equals($expected, $actual);
    if ($ok) {
        unset($_SESSION['_login_math']);
    } else {
        app_login_math_issue($username);
    }
    return $ok;
}

function create_password(string $password): string {
    return password_hash($password, PASSWORD_DEFAULT, ['cost' => 10]);
}

function verify_password(string $password, string $hash): bool {
    return password_verify($password, $hash);
}

function random_string(int $length = 16): string {
    return substr(bin2hex(random_bytes($length)), 0, $length);
}

function app_is_valid_ip(string $value): bool
{
    return filter_var($value, FILTER_VALIDATE_IP) !== false;
}

function app_ip_in_cidr(string $ipValue, string $cidr): bool
{
    $cidr = trim($cidr);
    if ($cidr === '' || !app_is_valid_ip($ipValue)) {
        return false;
    }
    if (strpos($cidr, '/') === false) {
        return $ipValue === $cidr;
    }

    [$subnet, $mask] = explode('/', $cidr, 2);
    $subnet = trim($subnet);
    $mask = (int)$mask;

    if (filter_var($ipValue, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        if ($mask < 0 || $mask > 32) {
            return false;
        }
        $ipLong = ip2long($ipValue);
        $subnetLong = ip2long($subnet);
        if ($ipLong === false || $subnetLong === false) {
            return false;
        }
        $netmask = $mask === 0 ? 0 : (-1 << (32 - $mask));
        return (($ipLong & $netmask) === ($subnetLong & $netmask));
    }

    return false;
}

function app_trusted_proxy_rules(): array
{
    $trusted = ['127.0.0.1', '::1'];
    $cfgTrusted = app_config()['trusted_proxies'] ?? [];
    if (is_string($cfgTrusted) && $cfgTrusted !== '') {
        $cfgTrusted = preg_split('/[\r\n,;]+/', $cfgTrusted);
    }
    if (is_array($cfgTrusted)) {
        foreach ($cfgTrusted as $item) {
            $item = trim((string)$item);
            if ($item !== '') {
                $trusted[] = $item;
            }
        }
    }
    return array_values(array_unique($trusted));
}

function app_is_trusted_proxy_ip(string $ip): bool
{
    $ip = trim($ip);
    if ($ip === '' || !app_is_valid_ip($ip)) {
        return false;
    }

    foreach (app_trusted_proxy_rules() as $rule) {
        if (app_ip_in_cidr($ip, (string)$rule)) {
            return true;
        }
    }
    return false;
}

function app_has_trusted_proxy_rules(): bool
{
    return app_trusted_proxy_rules() !== [];
}

function app_forwarded_proto_hint(): string
{
    $forwardedProto = trim((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
    if ($forwardedProto !== '') {
        $candidate = strtolower(trim((string)explode(',', $forwardedProto)[0]));
        if ($candidate === 'https' || $candidate === 'http') {
            return $candidate;
        }
    }

    $forwardedSsl = strtolower(trim((string)($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '')));
    if (in_array($forwardedSsl, ['on', '1', 'true', 'yes'], true)) {
        return 'https';
    }

    $frontEndHttps = strtolower(trim((string)($_SERVER['HTTP_FRONT_END_HTTPS'] ?? '')));
    if (in_array($frontEndHttps, ['on', '1', 'true', 'yes'], true)) {
        return 'https';
    }

    $cfVisitor = trim((string)($_SERVER['HTTP_CF_VISITOR'] ?? ''));
    if ($cfVisitor !== '') {
        $decoded = json_decode($cfVisitor, true);
        $scheme = is_array($decoded) ? strtolower(trim((string)($decoded['scheme'] ?? ''))) : '';
        if ($scheme === 'https' || $scheme === 'http') {
            return $scheme;
        }
    }

    $forwardedPort = trim((string)($_SERVER['HTTP_X_FORWARDED_PORT'] ?? ''));
    if ($forwardedPort === '443') {
        return 'https';
    }
    if ($forwardedPort === '80') {
        return 'http';
    }

    return '';
}

function app_request_host(): string
{
    $remoteIp = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));
    $trustForwarded = !app_has_trusted_proxy_rules() || app_is_trusted_proxy_ip($remoteIp);

    if ($trustForwarded) {
        foreach (['HTTP_X_FORWARDED_HOST', 'HTTP_X_ORIGINAL_HOST'] as $header) {
            $value = trim((string)($_SERVER[$header] ?? ''));
            if ($value !== '') {
                $host = trim((string)explode(',', $value)[0]);
                if ($host !== '') {
                    return $host;
                }
            }
        }
    }

    $host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
    if ($host !== '') {
        return $host;
    }

    return trim((string)($_SERVER['SERVER_NAME'] ?? ''));
}

function app_request_is_https(): bool
{
    if (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') {
        return true;
    }
    if ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443) {
        return true;
    }

    $remoteIp = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));
    $forwardedProto = app_forwarded_proto_hint();
    if ($forwardedProto === 'https' && (!app_has_trusted_proxy_rules() || app_is_trusted_proxy_ip($remoteIp))) {
        return true;
    }

    return false;
}

function app_client_ip(): string
{
    $ip = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));
    if ($ip === '') {
        return 'unknown';
    }

    if (app_is_trusted_proxy_ip($ip)) {
        $xff = trim((string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''));
        if ($xff !== '') {
            $parts = explode(',', $xff);
            foreach ($parts as $part) {
                $candidate = trim((string)$part);
                if ($candidate !== '' && app_is_valid_ip($candidate)) {
                    return $candidate;
                }
            }
        }
    }

    return app_is_valid_ip($ip) ? $ip : 'unknown';
}

function app_client_user_agent(): string
{
    return app_clean_text((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 512);
}

function app_destroy_session(): void
{
    if (session_status() !== PHP_SESSION_ACTIVE) {
        return;
    }

    $_SESSION = [];

    if (ini_get('session.use_cookies')) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', [
            'expires' => time() - 3600,
            'path' => $params['path'] ?? '/',
            'domain' => $params['domain'] ?? '',
            'secure' => !empty($params['secure']),
            'httponly' => !empty($params['httponly']),
            'samesite' => $params['samesite'] ?? 'Strict',
        ]);
    }

    session_destroy();
}

function app_normalize_device_serial(string $serial): string
{
    $serial = trim($serial);
    if ($serial === '') {
        return '';
    }

    $serial = str_replace(["\r", "\n", "\t", ',', ';', '|'], ' ', $serial);
    $serial = preg_replace('/\s+/', '', $serial) ?? '';
    $serial = trim($serial);
    if ($serial === '') {
        return '';
    }

    $serial = strtoupper($serial);
    $serial = preg_replace('/[^A-Z0-9._:@#-]/', '', $serial) ?? '';
    if ($serial === '') {
        return '';
    }

    if (strlen($serial) > 128) {
        $serial = substr($serial, 0, 128);
    }

    return $serial;
}

function app_normalize_device_list(string $raw): array
{
    $items = preg_split('/[\r\n,]+/', $raw) ?: [];
    $devices = [];
    $seen = [];
    foreach ($items as $item) {
        $serial = app_normalize_device_serial((string)$item);
        if ($serial === '' || isset($seen[$serial])) {
            continue;
        }
        $seen[$serial] = true;
        $devices[] = $serial;
    }
    return $devices;
}

function app_is_placeholder_device_serial(string $serial): bool
{
    $serial = app_normalize_device_serial($serial);
    if ($serial === '') {
        return true;
    }

    if (strlen($serial) < 6) {
        return true;
    }

    $placeholders = [
        'DEVICE',
        'DEVICEID',
        'DEVICE_ID',
        'DEVICE-ID',
        'DEVICECODE',
        'MACHINECODE',
        'MACHINE_CODE',
        'MACHINE-ID',
        'SERIAL',
        'UNKNOWN',
        'NONE',
        'NULL',
        'BROWSER',
        'WEB',
        'CLIENT',
        'TEST',
        'REALMACHINECODE',
        'REAL_MACHINE_CODE',
        'REAL-DEVICE-CODE',
        'YOURSERIAL',
        'YOUR_SERIAL',
        'YOURDEVICECODE',
        'YOUR_DEVICE_CODE',
        'YOURMACHINECODE',
        'YOUR_MACHINE_CODE',
        'EXAMPLE',
        'EXAMPLECODE',
        'SAMPLE',
        'DEMO',
    ];

    if (in_array($serial, $placeholders, true)) {
        return true;
    }

    if (str_contains($serial, 'YOUR_') || str_contains($serial, 'EXAMPLE') || str_contains($serial, 'DEMO')) {
        return true;
    }

    return (bool)preg_match('/^(AUTO|DEVICE|MACHINE|SERIAL|UNKNOWN|TEST)[-_A-Z0-9]*$/', $serial);
}

function app_can_use_browser_device_fallback(): bool
{
    if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
        return false;
    }

    $host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
    $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? ''));
    if ($host === '' || $method === '') {
        return false;
    }

    $userAgent = app_client_user_agent();
    if ($userAgent === '') {
        return false;
    }

    $ua = strtolower($userAgent);
    $nonBrowserAgents = [
        'curl/',
        'libcurl',
        'winhttp',
        'python-requests',
        'python-urllib',
        'postmanruntime',
        'insomnia',
        'okhttp',
        'java/',
        'go-http-client',
        'powershell',
        'wget/',
        'httpclient',
        'restsharp',
    ];
    foreach ($nonBrowserAgents as $needle) {
        if (str_contains($ua, $needle)) {
            return false;
        }
    }
    return true;
}

function app_browser_device_cookie_name(): string
{
    return 'ly_device_id';
}

function app_browser_device_serial_cookie_name(): string
{
    return 'ly_device_serial';
}

function app_browser_device_cookie_options(): array
{
    $secure = app_request_is_https();
    return [
        'expires' => time() + 86400 * 730,
        'path' => '/',
        'secure' => $secure,
        'httponly' => true,
        'samesite' => $secure ? 'None' : 'Lax',
    ];
}

function app_browser_device_seed(): string
{
    $cookieName = app_browser_device_cookie_name();
    $seed = trim((string)($_COOKIE[$cookieName] ?? ''));
    if ($seed !== '' && preg_match('/^[a-f0-9]{24,128}$/i', $seed)) {
        return strtolower($seed);
    }

    $seed = bin2hex(random_bytes(20));
    setcookie($cookieName, $seed, app_browser_device_cookie_options());
    $_COOKIE[$cookieName] = $seed;

    return $seed;
}

function app_browser_device_legacy_serial(): string
{
    $seed = app_browser_device_seed();
    $ua = app_client_user_agent();
    $lang = trim((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''));
    $host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
    $material = strtolower($seed . '|' . $ua . '|' . $lang . '|' . $host);

    return 'BROWSER-' . strtoupper(substr(hash('sha256', $material), 0, 32));
}

function app_browser_device_fingerprint_serial(): string
{
    $parts = [
        trim((string)($_SERVER['HTTP_USER_AGENT'] ?? '')),
        trim((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')),
        trim((string)($_SERVER['HTTP_SEC_CH_UA'] ?? '')),
        trim((string)($_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '')),
        trim((string)($_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION'] ?? '')),
        trim((string)($_SERVER['HTTP_SEC_CH_UA_ARCH'] ?? '')),
        trim((string)($_SERVER['HTTP_SEC_CH_UA_MOBILE'] ?? '')),
        trim((string)($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '')),
        trim((string)($_SERVER['HTTP_HOST'] ?? '')),
    ];

    $material = strtolower(implode('|', $parts));
    if ($material === '' || $material === str_repeat('|', count($parts) - 1)) {
        return '';
    }

    return 'BROWSER-' . strtoupper(substr(hash('sha256', $material), 0, 32));
}

function app_browser_device_serial(): string
{
    $cookieName = app_browser_device_serial_cookie_name();
    $cookieSerial = app_normalize_device_serial((string)($_COOKIE[$cookieName] ?? ''));
    if ($cookieSerial !== '' && preg_match('/^BROWSER-[A-Z0-9]{16,64}$/', $cookieSerial)) {
        return $cookieSerial;
    }

    $fingerprintSerial = app_normalize_device_serial(app_browser_device_fingerprint_serial());
    if ($fingerprintSerial !== '' && preg_match('/^BROWSER-[A-Z0-9]{16,64}$/', $fingerprintSerial)) {
        setcookie($cookieName, $fingerprintSerial, app_browser_device_cookie_options());
        $_COOKIE[$cookieName] = $fingerprintSerial;
        return $fingerprintSerial;
    }

    $seed = app_browser_device_seed();
    $lang = trim((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''));
    $host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
    $material = strtolower($seed . '|' . $host . '|' . $lang . '|light-year');
    $serial = 'BROWSER-' . strtoupper(substr(hash('sha256', $material), 0, 32));

    setcookie($cookieName, $serial, app_browser_device_cookie_options());
    $_COOKIE[$cookieName] = $serial;

    return $serial;
}

function app_resolve_login_device_serial_context(?string $providedSerial): array
{
    $serial = app_normalize_device_serial((string)$providedSerial);
    if ($serial !== '' && !app_is_placeholder_device_serial($serial)) {
        return [
            'serial' => $serial,
            'aliases' => [$serial],
            'fallback' => false,
        ];
    }

    if (app_can_use_browser_device_fallback()) {
        $current = app_normalize_device_serial(app_browser_device_serial());
        $aliases = [$current];
        $legacy = app_normalize_device_serial(app_browser_device_legacy_serial());
        if ($legacy !== '' && !in_array($legacy, $aliases, true)) {
            $aliases[] = $legacy;
        }
        $fingerprint = app_normalize_device_serial(app_browser_device_fingerprint_serial());
        if ($fingerprint !== '' && !in_array($fingerprint, $aliases, true)) {
            $aliases[] = $fingerprint;
        }

        return [
            'serial' => $current,
            'aliases' => $aliases,
            'fallback' => true,
        ];
    }

    return [
        'serial' => '',
        'aliases' => [],
        'fallback' => false,
    ];
}

function app_resolve_login_device_serial(?string $providedSerial): string
{
    $context = app_resolve_login_device_serial_context($providedSerial);
    return (string)($context['serial'] ?? '');
}

function app_rate_limit_consume(string $scope, int $limit, int $windowSec, string $identity = ''): array
{
    return app_rate_limiter()->consume($scope, $limit, $windowSec, $identity !== '' ? $identity : app_client_ip());
}

function app_rate_limiter(): \App\Security\RateLimiterInterface
{
    static $limiter = null;
    if ($limiter instanceof \App\Security\RateLimiterInterface) {
        return $limiter;
    }

    app_ensure_bridge_security_runtime();
    $cfg = app_security_config();
    $rateCfg = is_array($cfg['rate_limit'] ?? null) ? (array)$cfg['rate_limit'] : [];
    $storageDir = __DIR__ . '/../storage/cache/ratelimit';

    $limiter = \App\Security\RateLimiterFactory::create($rateCfg, $storageDir);
    return $limiter;
}

function app_api_secret(): string
{
    app_ensure_security_runtime();
    $cfg = app_config();
    $secret = trim((string)($cfg['api_secret'] ?? ''));
    if ($secret !== '' && $secret !== 'change_this_to_a_random_api_secret') {
        return $secret;
    }

    $envSecret = trim((string)(getenv('APP_API_SECRET') ?: getenv('API_SECRET') ?: ''));
    if ($envSecret !== '') {
        return $envSecret;
    }

    return '';
}

function app_api_key_registry(): array
{
    app_ensure_bridge_security_runtime();
    $cfg = app_security_config();
    $registry = [];

    $primary = app_api_secret();
    if ($primary !== '') {
        $registry['default'] = $primary;
    }

    $keys = is_array($cfg['api']['keys'] ?? null) ? (array)$cfg['api']['keys'] : [];
    foreach ($keys as $row) {
        if (!is_array($row)) {
            continue;
        }
        $id = app_clean_text((string)($row['id'] ?? ''), 64);
        $secret = trim((string)($row['secret'] ?? ''));
        if ($id === '' || $secret === '') {
            continue;
        }
        $registry[$id] = $secret;
    }

    return $registry;
}

function app_api_signature_window(): int
{
    app_ensure_bridge_security_runtime();
    $cfg = app_security_config();
    return max(60, (int)($cfg['api']['timestamp_window'] ?? 300));
}

function app_api_nonce_ttl(): int
{
    app_ensure_bridge_security_runtime();
    $cfg = app_security_config();
    return max(60, (int)($cfg['api']['nonce_ttl'] ?? 300));
}

function app_register_nonce_once(string $scope, int $ttlSec): bool
{
    $scope = trim($scope);
    if ($scope === '') {
        return false;
    }

    $dir = __DIR__ . '/../storage/cache/nonces';
    app_safe_mkdir($dir, 0755);
    $file = $dir . '/' . hash('sha256', $scope) . '.nonce';
    $fp = @fopen($file, 'c+');
    if ($fp === false) {
        return true;
    }

    $now = time();
    try {
        if (!@flock($fp, LOCK_EX)) {
            return true;
        }
        $existing = trim((string)stream_get_contents($fp));
        $last = ctype_digit($existing) ? (int)$existing : 0;
        if ($last > 0 && ($now - $last) < $ttlSec) {
            @flock($fp, LOCK_UN);
            return false;
        }

        rewind($fp);
        @ftruncate($fp, 0);
        @fwrite($fp, (string)$now);
        @fflush($fp);
        @flock($fp, LOCK_UN);
    } finally {
        @fclose($fp);
    }

    return true;
}

function app_consume_optional_client_nonce(string $scope, ?string $nonce, int $ttlSec = 60): bool
{
    $nonce = app_clean_text((string)$nonce, 96);
    if ($nonce === '') {
        return true;
    }

    if (!preg_match('/^[A-Za-z0-9._:-]{8,96}$/', $nonce)) {
        app_log_security_event('client_nonce_rejected', [
            'scope' => $scope,
            'reason' => 'invalid_format',
        ]);
        return false;
    }

    $ok = app_register_nonce_once('client|' . trim($scope) . '|' . $nonce, max(60, $ttlSec));
    if (!$ok) {
        app_log_security_event('client_nonce_rejected', [
            'scope' => $scope,
            'reason' => 'replayed',
        ]);
    }

    return $ok;
}

function app_token_secret(): string
{
    app_ensure_security_runtime();
    $cfg = app_config();
    $salt = trim((string)($cfg['token_salt'] ?? ''));
    if ($salt !== '' && $salt !== 'change_this_to_a_random_string') {
        return $salt;
    }

    $apiSecret = app_api_secret();
    if ($apiSecret !== '') {
        return $apiSecret;
    }

    $db = db_config();
    $mixed = implode('|', [
        (string)($db['host'] ?? ''),
        (string)($db['database'] ?? ''),
        (string)($db['username'] ?? ''),
        (string)($db['password'] ?? ''),
        (string)php_uname('n'),
        __DIR__,
    ]);

    return hash('sha256', $mixed);
}

function token_v1_for(string $game, string $userKey, string $serial): string
{
    $data = $game . '-' . $userKey . '-' . $serial;
    return hash_hmac('sha256', $data, app_token_secret());
}

function token_v2_for(string $game, string $userKey, string $serial, int $ttl = 900): string
{
    $now = time();
    try {
        $nonce = bin2hex(random_bytes(12));
    } catch (Throwable $e) {
        $nonce = substr(app_generate_secret(16), 0, 24);
    }
    $payload = [
        'v' => 2,
        'game' => app_clean_text($game, 64),
        'user_key' => app_clean_text($userKey, 128),
        'serial' => app_clean_text($serial, 128),
        'iat' => $now,
        'exp' => $now + max(60, $ttl),
        'nonce' => $nonce,
    ];
    $json = app_canonical_json($payload);
    $sig = hash_hmac('sha256', $json, app_token_secret(), true);
    return 'v2.' . app_b64url_encode($json) . '.' . app_b64url_encode($sig);
}

function token_parse_any(string $token, string $game = '', string $userKey = '', string $serial = ''): ?array
{
    $token = trim($token);
    if ($token === '') {
        return null;
    }

    if (str_starts_with($token, 'v2.')) {
        $parts = explode('.', $token, 3);
        if (count($parts) !== 3) {
            return null;
        }

        $json = app_b64url_decode((string)$parts[1]);
        $sig = app_b64url_decode((string)$parts[2]);
        if ($json === '' || $sig === '') {
            return null;
        }

        $expected = hash_hmac('sha256', $json, app_token_secret(), true);
        if (!hash_equals($expected, $sig)) {
            return null;
        }

        $payload = json_decode($json, true);
        if (!is_array($payload)) {
            return null;
        }

        if ((int)($payload['exp'] ?? 0) < time()) {
            return null;
        }
        if ($game !== '' && !hash_equals(app_clean_text($game, 64), (string)($payload['game'] ?? ''))) {
            return null;
        }
        if ($userKey !== '' && !hash_equals(app_clean_text($userKey, 128), (string)($payload['user_key'] ?? ''))) {
            return null;
        }
        if ($serial !== '' && !hash_equals(app_clean_text($serial, 128), (string)($payload['serial'] ?? ''))) {
            return null;
        }

        return ['version' => 2, 'payload' => $payload];
    }

    if ($game === '' || $userKey === '' || $serial === '') {
        return null;
    }

    $expected = token_v1_for($game, $userKey, $serial);
    if (!hash_equals($expected, $token)) {
        return null;
    }

    return [
        'version' => 1,
        'payload' => [
            'game' => $game,
            'user_key' => $userKey,
            'serial' => $serial,
        ],
    ];
}

function token_for(string $game, string $userKey, string $serial): string
{
    return token_v1_for($game, $userKey, $serial);
}

function app_api_signature_base(string $method, string $path, string $keyId, string $timestamp, string $nonce, string $bodyHash): string
{
    $query = $_GET;
    foreach (['token', 'api_token', 'api_key', 'key'] as $sensitive) {
        unset($query[$sensitive]);
    }
    ksort($query);
    $queryJson = json_encode($query, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if (!is_string($queryJson)) {
        $queryJson = '{}';
    }

    return strtoupper(trim($method)) . "\n"
        . $path . "\n"
        . trim($keyId) . "\n"
        . trim($timestamp) . "\n"
        . trim($nonce) . "\n"
        . $queryJson . "\n"
        . $bodyHash;
}

function app_verify_signed_api_request(): array
{
    $keyId = app_clean_text((string)($_SERVER['HTTP_X_API_KEY_ID'] ?? ''), 64);
    $timestamp = trim((string)($_SERVER['HTTP_X_API_TIMESTAMP'] ?? ''));
    $nonce = app_clean_text((string)($_SERVER['HTTP_X_API_NONCE'] ?? ''), 96);
    $sign = trim((string)($_SERVER['HTTP_X_API_SIGN'] ?? ''));

    if ($keyId === '' || $timestamp === '' || $nonce === '' || $sign === '') {
        app_log_security_event('signed_api_failed', ['reason' => 'missing_signed_headers']);
        return ['ok' => false, 'reason' => 'missing_signed_headers'];
    }
    if (!ctype_digit($timestamp)) {
        app_log_security_event('signed_api_failed', ['reason' => 'invalid_timestamp']);
        return ['ok' => false, 'reason' => 'invalid_timestamp'];
    }

    $window = app_api_signature_window();
    if (abs(time() - (int)$timestamp) > $window) {
        app_log_security_event('signed_api_failed', ['reason' => 'signature_expired', 'key_id' => $keyId]);
        return ['ok' => false, 'reason' => 'signature_expired'];
    }

    $registry = app_api_key_registry();
    $secret = (string)($registry[$keyId] ?? '');
    if ($secret === '') {
        app_log_security_event('signed_api_failed', ['reason' => 'key_not_found', 'key_id' => $keyId]);
        return ['ok' => false, 'reason' => 'key_not_found'];
    }

    $rawBody = file_get_contents('php://input');
    $bodyHash = hash('sha256', is_string($rawBody) ? $rawBody : '');
    $uri = (string)($_SERVER['REQUEST_URI'] ?? '/');
    $path = (string)(parse_url($uri, PHP_URL_PATH) ?: '/');
    $base = app_api_signature_base((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'), $path, $keyId, $timestamp, $nonce, $bodyHash);
    $expected = hash_hmac('sha256', $base, $secret);
    if (!hash_equals(strtolower($expected), strtolower($sign))) {
        app_log_security_event('signed_api_failed', ['reason' => 'signature_verify_failed', 'key_id' => $keyId]);
        return ['ok' => false, 'reason' => 'signature_verify_failed'];
    }

    $nonceOk = app_register_nonce_once('api|' . $keyId . '|' . $nonce, app_api_nonce_ttl());
    if (!$nonceOk) {
        app_log_security_event('signed_api_failed', ['reason' => 'signature_replayed', 'key_id' => $keyId]);
        return ['ok' => false, 'reason' => 'signature_replayed'];
    }

    app_log_api_event('signed_api_verified', ['key_id' => $keyId]);
    return ['ok' => true, 'key_id' => $keyId];
}

function station_normalize_version_label(string $version): string
{
    $version = strtolower(trim($version));
    if ($version === '') {
        return '';
    }

    // Normalize common punctuation variants without depending on garbled locale literals.
    $version = str_replace(["\xEF\xBC\x8E", "\xE3\x80\x82", ' '], ['.', '.', ''], $version);
    $version = (string)preg_replace('/^v[\s._-]*/i', '', $version);
    $version = str_replace('_', '.', $version);
    $version = (string)preg_replace('/[^0-9a-z.+-]/i', '', $version);
    $version = (string)preg_replace('/\.+/', '.', $version);
    $version = trim($version, ".-+");

    return $version;
}

function station_version_is_timestamp_like(string $version): bool
{
    $normalized = station_normalize_version_label($version);
    if ($normalized === '') {
        return false;
    }

    $digits = str_replace(['.', '-', '+'], '', $normalized);
    return preg_match('/^[0-9]{8,}$/', $digits) === 1;
}

function station_version_is_semantic_like(string $version): bool
{
    $normalized = station_normalize_version_label($version);
    if ($normalized === '') {
        return false;
    }
    if (station_version_is_timestamp_like($normalized)) {
        return false;
    }

    return preg_match('/^\d+(?:\.\d+){0,5}(?:-[0-9a-z.-]+)?$/i', $normalized) === 1;
}

function station_is_version_newer(string $latest, string $current): bool
{
    $latestRaw = trim($latest);
    $currentRaw = trim($current);
    if ($latestRaw === '' || strcasecmp($latestRaw, $currentRaw) === 0) {
        return false;
    }

    $latest = station_normalize_version_label($latestRaw);
    $current = station_normalize_version_label($currentRaw);
    if ($latest === '' || $current === '') {
        return strnatcasecmp($latestRaw, $currentRaw) > 0;
    }
    if (strcasecmp($latest, $current) === 0) {
        return false;
    }

    if (
        preg_match('/^[0-9]+(?:\.[0-9]+)*(?:-[0-9a-z.-]+)?$/i', $latest)
        && preg_match('/^[0-9]+(?:\.[0-9]+)*(?:-[0-9a-z.-]+)?$/i', $current)
    ) {
        if (station_version_is_timestamp_like($current) && station_version_is_semantic_like($latest)) {
            return true;
        }
        if (station_version_is_timestamp_like($latest) && station_version_is_semantic_like($current)) {
            return false;
        }
        return version_compare($latest, $current, '>');
    }

    $latestDigits = str_replace(['.', '-', '+'], '', $latest);
    $currentDigits = str_replace(['.', '-', '+'], '', $current);
    $latestIsTimestamp = preg_match('/^[0-9]{8,}$/', $latestDigits) === 1;
    $currentIsTimestamp = preg_match('/^[0-9]{8,}$/', $currentDigits) === 1;
    $latestIsSemantic = station_version_is_semantic_like($latest);
    $currentIsSemantic = station_version_is_semantic_like($current);

    if ($latestIsSemantic && $currentIsTimestamp) {
        return true;
    }
    if ($latestIsTimestamp && $currentIsSemantic) {
        return false;
    }

    if ($latestIsTimestamp && $currentIsTimestamp) {
        return (int)$latestDigits > (int)$currentDigits;
    }

    preg_match_all('/\d+/', $latest, $latestNums);
    preg_match_all('/\d+/', $current, $currentNums);
    $la = array_map('intval', $latestNums[0] ?? []);
    $ca = array_map('intval', $currentNums[0] ?? []);
    if ($la && $ca) {
        $len = max(count($la), count($ca));
        for ($i = 0; $i < $len; $i++) {
            $lv = $la[$i] ?? 0;
            $cv = $ca[$i] ?? 0;
            if ($lv === $cv) {
                continue;
            }
            return $lv > $cv;
        }
        return false;
    }

    return strnatcasecmp($latest, $current) > 0;
}

function station_current_domain(): string
{
    $serverKeys = ['HTTP_X_FORWARDED_HOST', 'HTTP_X_ORIGINAL_HOST', 'HTTP_HOST'];
    foreach ($serverKeys as $serverKey) {
        $rawHost = strtolower(trim((string)($_SERVER[$serverKey] ?? '')));
        if ($rawHost === '') {
            continue;
        }

        $candidates = array_map('trim', explode(',', $rawHost));
        foreach ($candidates as $candidate) {
            $candidate = preg_replace('/:\\d+$/', '', $candidate);
            $candidate = trim((string)$candidate);
            if ($candidate === '' || $candidate === 'localhost' || $candidate === '127.0.0.1') {
                continue;
            }
            return $candidate;
        }
    }

    $base = trim((string)(app_config()['base_url'] ?? ''));
    if ($base !== '' && preg_match('#^https?://#i', $base)) {
        $parsed = parse_url($base, PHP_URL_HOST);
        if (is_string($parsed) && $parsed !== '') {
            return strtolower($parsed);
        }
    }

    return '';
}

function station_update_server_candidates(): array
{
    $api = station_update_server();
    if ($api === '') {
        return [];
    }

    $candidates = [$api];
    $parsed = parse_url($api);
    if (is_array($parsed)) {
        $host = trim((string)($parsed['host'] ?? ''));
        $path = (string)($parsed['path'] ?? '');
        $scheme = strtolower(trim((string)($parsed['scheme'] ?? '')));
        $port = array_key_exists('port', $parsed) ? (int)$parsed['port'] : 0;
        $query = (string)($parsed['query'] ?? '');

        if ($host !== '' && ($path === '' || $path === '/' || $path === '\\')) {
            $base = ($scheme !== '' ? $scheme : 'https') . '://' . $host;
            if ($port > 0) {
                $base .= ':' . $port;
            }
            foreach (['/public/api.php', '/api.php'] as $suffix) {
                $candidate = $base . $suffix;
                if ($query !== '') {
                    $candidate .= '?' . $query;
                }
                if (!in_array($candidate, $candidates, true)) {
                    $candidates[] = $candidate;
                }
            }
        }
    }

    return $candidates;
}

function station_update_server_pin_key(): string
{
    return 'station_update_api_pin_' . md5(trim((string)station_update_server()));
}

function station_update_server_pinned_candidate(): string
{
    return station_update_server();
}

function station_update_server_pin_success(string $server): void
{
    // single fixed update API entry: no dynamic pinning needed
}

function station_update_log_path(): string
{
    $dir = __DIR__ . '/../storage/update';
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    return $dir . '/update_client.log';
}

function station_update_log(array $context): void
{
    $parts = ['[Update]'];
    foreach ($context as $key => $value) {
        if (is_array($value) || is_object($value)) {
            $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        $parts[] = $key . '=' . str_replace(["\r", "\n"], [' ', ' '], app_mask_sensitive_text((string)$value));
    }
    @file_put_contents(station_update_log_path(), implode(' ', $parts) . PHP_EOL, FILE_APPEND);
}

function station_update_request_config(): array
{
    $security = app_security_config();
    $update = is_array($security['update'] ?? null) ? $security['update'] : [];
    $connectTimeout = (int)($update['connect_timeout'] ?? 5);
    $timeout = (int)($update['timeout'] ?? 12);
    $retries = (int)($update['retries'] ?? 2);

    return [
        'connect_timeout' => max(3, $connectTimeout),
        'timeout' => max(6, $timeout),
        'retries' => max(0, $retries),
    ];
}

function station_update_request_headers(string $domain, string $currentVersion = '', string $target = 'online'): array
{
    $headers = station_bridge_headers($domain, $currentVersion, $target);
    $headers[] = 'Accept: application/json';
    $headers[] = 'X-Client-Capabilities: signed-package';
    $headers[] = 'X-Package-Signature: 1';
    return array_values(array_unique($headers));
}

function station_update_header_mode(array $headers): string
{
    return station_header_pick($headers, 'authorization') !== '' ? 'bearer' : (station_header_pick($headers, 'x-api-token') !== '' ? 'legacy-token' : 'none');
}

function station_update_response_reason(array $result): string
{
    $status = (int)($result['status'] ?? 0);
    $error = trim((string)($result['error'] ?? ''));
    $json = is_array($result['json'] ?? null) ? (array)$result['json'] : [];

    if ($error !== '') {
        return $error;
    }
    if ($status === 498) {
        return 'signature_verify_failed';
    }
    if ($status === 0) {
        return 'network_error';
    }
    if (!empty($json['reason'])) {
        return trim((string)$json['reason']);
    }
    if ($status !== 200) {
        return 'http_' . $status;
    }
    if (empty($json['status'])) {
        return 'status_false';
    }
    return '';
}

function station_update_response_is_compat_retryable(array $result): bool
{
    $status = (int)($result['status'] ?? 0);
    $error = strtolower(trim((string)($result['error'] ?? '')));
    $reason = strtolower(station_update_response_reason($result));

    if ($status === 0) {
        return true;
    }
    if (in_array($status, [301, 302, 307, 308, 404, 405, 500, 502, 503, 504], true)) {
        return true;
    }
    if ($error !== '' && (
        str_contains($error, 'stream_request_failed')
        || str_contains($error, 'non_json_html_response')
        || str_contains($error, 'non_json_response')
        || str_contains($error, 'network')
    )) {
        return true;
    }
    return in_array($reason, ['network_error', 'http_301', 'http_302', 'http_307', 'http_308', 'http_404', 'http_405', 'http_500', 'http_502', 'http_503', 'http_504', 'non_json_html_response', 'non_json_response'], true);
}

function station_update_body_preview(string $body, int $limit = 240): string
{
    $body = trim($body);
    if ($body === '') {
        return '';
    }

    $body = preg_replace('/\s+/u', ' ', $body) ?? $body;
    if (function_exists('mb_substr')) {
        return mb_substr($body, 0, $limit, 'UTF-8');
    }

    return substr($body, 0, $limit);
}

function requestUpdateAPI(string $action, array $params = []): array
{
    $domain = trim((string)($params['domain'] ?? station_current_domain()));
    $currentVersion = trim((string)($params['current_version'] ?? (app_config()['app_version'] ?? '')));
    $target = trim((string)($params['target'] ?? 'online'));
    $requestCfg = station_update_request_config();
    $headers = station_update_request_headers($domain, $currentVersion, $target);
    $headerMode = station_update_header_mode($headers);

    $apiCandidates = station_update_server_candidates();
    if (!$apiCandidates) {
        $result = [
            'ok' => false,
            'status' => 0,
            'json' => [],
            'body' => '',
            'error' => 'update_api_url_missing',
            'url' => '',
            'action' => $action,
            'time_ms' => 0,
            'header_mode' => $headerMode,
            'network_error' => false,
        ];
        station_cache_set('station_update_debug_last', $result);
        station_update_log([
            'action' => $action,
            'url' => '',
            'status' => 0,
            'time_ms' => 0,
            'error' => 'update_api_url_missing',
            'header' => $headerMode,
        ]);
        return $result;
    }

    $query = $params;
    $query['action'] = $action;
    if (!array_key_exists('_t', $query)) {
        $query['_t'] = time();
    }
    if (!array_key_exists('_cb', $query)) {
        $query['_cb'] = station_cache_bust_token();
    }

    $attempts = max(1, $requestCfg['retries'] + 1);
    $lastResult = [
        'ok' => false,
        'status' => 0,
        'json' => [],
        'body' => '',
        'error' => 'update_api_url_missing',
        'url' => '',
        'action' => $action,
        'time_ms' => 0,
        'header_mode' => $headerMode,
        'network_error' => false,
        'effective_url' => '',
        'redirect_count' => 0,
        'body_preview' => '',
    ];

    foreach ($apiCandidates as $candidateApi) {
        $url = $candidateApi . (strpos($candidateApi, '?') === false ? '?' : '&') . http_build_query($query);
        $signedHeaders = function_exists('station_bridge_request_signature_headers_for_request')
            ? station_bridge_request_signature_headers_for_request('GET', $url, $domain, '')
            : [];
        $candidateHeaders = $headers;
        if (is_array($signedHeaders) && $signedHeaders) {
            $candidateHeaders = array_values(array_unique(array_merge($candidateHeaders, $signedHeaders)));
        }

        station_update_log([
            'action' => $action,
            'url' => $url,
            'status' => 'start',
            'time_ms' => 0,
            'error' => '',
            'header' => $headerMode,
        ]);

        $fetchOnce = static function () use ($url, $candidateHeaders, $requestCfg): array {
            $started = microtime(true);
            if (function_exists('curl_init')) {
                $ch = curl_init();
                curl_setopt_array($ch, [
                    CURLOPT_URL => $url,
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_FOLLOWLOCATION => true,
                    CURLOPT_TIMEOUT => (int)$requestCfg['timeout'],
                    CURLOPT_CONNECTTIMEOUT => (int)$requestCfg['connect_timeout'],
                    CURLOPT_HTTPHEADER => $candidateHeaders,
                    CURLOPT_SSL_VERIFYPEER => true,
                    CURLOPT_SSL_VERIFYHOST => 2,
                    CURLOPT_NOSIGNAL => true,
                ]);
                $body = curl_exec($ch);
                $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
                $effectiveUrl = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
                $redirectCount = (int)curl_getinfo($ch, CURLINFO_REDIRECT_COUNT);
                $error = curl_error($ch) ?: '';
                curl_close($ch);
                return [
                    'status' => $status,
                    'body' => $body === false ? '' : (string)$body,
                    'error' => $body === false ? $error : '',
                    'time_ms' => (int)round((microtime(true) - $started) * 1000),
                    'effective_url' => $effectiveUrl,
                    'redirect_count' => $redirectCount,
                ];
            }

            $context = stream_context_create([
                'http' => [
                    'method' => 'GET',
                    'header' => implode("\r\n", $candidateHeaders),
                    'timeout' => (int)$requestCfg['timeout'],
                    'ignore_errors' => true,
                ],
                'ssl' => [
                    'verify_peer' => true,
                    'verify_peer_name' => true,
                    'allow_self_signed' => false,
                ],
            ]);
            $body = @file_get_contents($url, false, $context);
            $status = 0;
            if (!empty($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
                $status = (int)$m[1];
            }
            return [
                'status' => $status,
                'body' => $body === false ? '' : (string)$body,
                'error' => $body === false ? 'stream_request_failed' : '',
                'time_ms' => (int)round((microtime(true) - $started) * 1000),
                'effective_url' => $url,
                'redirect_count' => 0,
            ];
        };

        $last = [
            'status' => 0,
            'body' => '',
            'error' => '',
            'time_ms' => 0,
            'effective_url' => '',
            'redirect_count' => 0,
        ];
        for ($attempt = 0; $attempt < $attempts; $attempt++) {
            $last = $fetchOnce();
            $networkError = ((int)$last['status'] === 0) || trim((string)$last['error']) !== '';
            if (!$networkError || $attempt + 1 >= $attempts) {
                break;
            }
            usleep(200000 * ($attempt + 1));
        }

        $json = json_decode((string)$last['body'], true);
        if (!is_array($json)) {
            $json = [];
            if ((int)$last['status'] === 200 && trim((string)$last['body']) !== '') {
                $preview = strtolower(station_update_body_preview((string)$last['body'], 120));
                if (str_contains($preview, '<html') || str_contains($preview, '<!doctype') || str_contains($preview, '<body')) {
                    $last['error'] = 'non_json_html_response';
                } else {
                    $last['error'] = trim((string)$last['error']) !== '' ? (string)$last['error'] : 'non_json_response';
                }
            }
        }

        if ((int)$last['status'] === 200 && $json && in_array($action, ['authorize', 'check', 'public_info'], true) && function_exists('station_bridge_verify_signed_payload')) {
            if ($domain !== '' && !station_bridge_verify_signed_payload($json, $action, $domain)) {
                $json = [
                    'status' => false,
                    'reason' => 'signature_verify_failed',
                ];
                $last['status'] = 498;
                $last['error'] = 'signature_verify_failed';
            }
        }

        $lastResult = [
            'ok' => ((int)$last['status'] === 200) && !empty($json['status']),
            'status' => (int)$last['status'],
            'json' => $json,
            'body' => (string)$last['body'],
            'error' => trim((string)$last['error']) !== '' ? (string)$last['error'] : station_update_response_reason(['status' => $last['status'], 'json' => $json, 'error' => $last['error']]),
            'url' => $url,
            'action' => $action,
            'time_ms' => (int)($last['time_ms'] ?? 0),
            'header_mode' => $headerMode,
            'network_error' => (((int)$last['status'] === 0) || trim((string)$last['error']) !== ''),
            'effective_url' => trim((string)($last['effective_url'] ?? '')),
            'redirect_count' => (int)($last['redirect_count'] ?? 0),
            'body_preview' => station_update_body_preview((string)($last['body'] ?? '')),
        ];

        if ($lastResult['ok'] || !station_update_response_is_compat_retryable($lastResult)) {
            break;
        }
    }

    station_cache_set('station_update_debug_last', $lastResult);
    station_update_log([
        'action' => $action,
        'url' => (string)($lastResult['url'] ?? ''),
        'effective_url' => (string)($lastResult['effective_url'] ?? ''),
        'redirect_count' => (int)($lastResult['redirect_count'] ?? 0),
        'status' => (int)$lastResult['status'],
        'time_ms' => (int)$lastResult['time_ms'],
        'error' => (string)$lastResult['error'],
        'header' => $headerMode,
        'preview' => (string)($lastResult['body_preview'] ?? ''),
    ]);

    return $lastResult;
}

function fetchUpdateState(bool $forceRefresh = false): array
{
    static $runtime = null;

    $domain = station_current_domain();
    $currentVersion = trim((string)(app_config()['app_version'] ?? ''));
    $api = station_update_server();
    $scope = md5($domain . '|' . $currentVersion . '|' . $api);
    $successCacheKey = 'station_update_state_ok_v1_' . $scope;
    $failureCacheKey = 'station_update_state_fail_v1_' . $scope;

    if (!$forceRefresh && is_array($runtime)) {
        return $runtime;
    }

    if (!$forceRefresh) {
        $cachedSuccess = station_cache_get($successCacheKey, 120);
        if (is_array($cachedSuccess) && isset($cachedSuccess['authorized'])) {
            $cachedSuccess['cache_hit'] = 'success';
            $runtime = $cachedSuccess;
            return $runtime;
        }
        $cachedFailure = station_cache_get($failureCacheKey, 10);
        if (is_array($cachedFailure) && array_key_exists('authorized', $cachedFailure)) {
            $cachedFailure['cache_hit'] = 'failure';
            $runtime = $cachedFailure;
            return $runtime;
        }
    }

    $state = [
        'authorized' => false,
        'latest_version' => '',
        'download_url' => '',
        'has_update' => false,
        'error' => null,
        'current_version' => $currentVersion,
        'update_api_url' => $api,
        'checked_at' => date('Y-m-d H:i:s'),
        'cache_hit' => 'miss',
        'latest' => null,
        'site_notice' => null,
        'request_trace' => [],
    ];

    if ($domain === '') {
        $state['error'] = 'domain_missing';
        station_cache_set($failureCacheKey, $state);
        $runtime = $state;
        return $runtime;
    }
    if ($api === '') {
        $state['error'] = 'update_api_url_missing';
        station_cache_set($failureCacheKey, $state);
        $runtime = $state;
        return $runtime;
    }

    $authorize = requestUpdateAPI('authorize', [
        'domain' => $domain,
    ]);
    $check = requestUpdateAPI('check', [
        'domain' => $domain,
        'current_version' => $currentVersion,
        'target' => 'online',
    ]);
    $publicInfo = requestUpdateAPI('public_info', [
        'domain' => $domain,
        'current_version' => $currentVersion,
        'target' => 'online',
    ]);

    $state['request_trace'] = [
        'authorize' => [
            'status' => (int)$authorize['status'],
            'ok' => !empty($authorize['ok']),
            'error' => (string)$authorize['error'],
            'url' => (string)$authorize['url'],
            'time_ms' => (int)$authorize['time_ms'],
            'header' => (string)$authorize['header_mode'],
            'debug' => is_array($authorize['json']['debug'] ?? null) ? (array)$authorize['json']['debug'] : null,
        ],
        'check' => [
            'status' => (int)$check['status'],
            'ok' => !empty($check['ok']),
            'error' => (string)$check['error'],
            'url' => (string)$check['url'],
            'time_ms' => (int)$check['time_ms'],
            'header' => (string)$check['header_mode'],
            'debug' => is_array($check['json']['debug'] ?? null) ? (array)$check['json']['debug'] : null,
        ],
        'public_info' => [
            'status' => (int)$publicInfo['status'],
            'ok' => !empty($publicInfo['ok']),
            'error' => (string)$publicInfo['error'],
            'url' => (string)$publicInfo['url'],
            'time_ms' => (int)$publicInfo['time_ms'],
            'header' => (string)$publicInfo['header_mode'],
            'debug' => is_array($publicInfo['json']['debug'] ?? null) ? (array)$publicInfo['json']['debug'] : null,
        ],
    ];

    $state['authorized'] = !empty($authorize['ok']) || !empty($check['ok']);

    $latestCheck = is_array($check['json']['latest'] ?? null) ? (array)$check['json']['latest'] : null;
    $latestPublic = is_array($publicInfo['json']['latest'] ?? null) ? (array)$publicInfo['json']['latest'] : null;

    $payloadScore = static function (?array $payload): int {
        if (!is_array($payload)) {
            return -1;
        }
        $score = 0;
        foreach (['update_note', 'optimize_note', 'fix_note', 'update_items', 'optimize_items', 'fix_items'] as $key) {
            if (station_release_note_text($payload[$key] ?? '') !== '') {
                $score += 2;
            }
        }
        if (trim((string)($payload['download_url'] ?? $payload['package_url'] ?? $payload['url'] ?? '')) !== '') {
            $score += 2;
        }
        if (trim((string)($payload['sha256'] ?? $payload['package_sha256'] ?? '')) !== '') {
            $score += 1;
        }
        return $score;
    };

    $latest = null;
    $selectionSource = '';
    $selectionDebug = null;
    if (is_array($latestCheck) && is_array($latestPublic)) {
        $checkVersion = trim((string)($latestCheck['version'] ?? ''));
        $publicVersion = trim((string)($latestPublic['version'] ?? ''));
        if ($checkVersion !== '' && $publicVersion !== '') {
            if (station_is_version_newer($checkVersion, $publicVersion)) {
                $latest = $latestCheck;
                $selectionSource = 'check';
            } elseif (station_is_version_newer($publicVersion, $checkVersion)) {
                $latest = $latestPublic;
                $selectionSource = 'public_info';
            } else {
                if ($payloadScore($latestPublic) >= $payloadScore($latestCheck)) {
                    $latest = $latestPublic;
                    $selectionSource = 'public_info';
                } else {
                    $latest = $latestCheck;
                    $selectionSource = 'check';
                }
            }
        } else {
            if ($payloadScore($latestPublic) >= $payloadScore($latestCheck)) {
                $latest = $latestPublic;
                $selectionSource = 'public_info';
            } else {
                $latest = $latestCheck;
                $selectionSource = 'check';
            }
        }
        if (is_array($latest)) {
            $siteNotice = $publicInfo['json']['site_notice'] ?? ($check['json']['site_notice'] ?? null);
            if ((empty($latest['site_notice']) || !is_array($latest['site_notice'])) && is_array($siteNotice)) {
                $latest['site_notice'] = $siteNotice;
            }
        }
    } elseif (is_array($latestCheck)) {
        $latest = $latestCheck;
        $selectionSource = 'check';
    } elseif (is_array($latestPublic)) {
        $latest = $latestPublic;
        $selectionSource = 'public_info';
    }

    $siteNotice = $publicInfo['json']['site_notice'] ?? ($check['json']['site_notice'] ?? null);
    $state['site_notice'] = is_array($siteNotice) ? $siteNotice : null;

    if (is_array($latest)) {
        $latest['_server_used'] = $api;
        $state['latest'] = $latest;
        $state['latest_version'] = trim((string)($latest['version'] ?? $latest['latest_version'] ?? $latest['version_name'] ?? ''));
        $state['download_url'] = trim((string)($latest['download_url'] ?? $latest['package_url'] ?? $latest['url'] ?? ''));
        $state['has_update'] = $state['latest_version'] !== '' && station_is_version_newer($state['latest_version'], $currentVersion);
    }
    $selectionDebug = [];
    if ($selectionSource === 'check') {
        $selectionDebug = (array)($check['json']['debug'] ?? []);
    } elseif ($selectionSource === 'public_info') {
        $selectionDebug = (array)($publicInfo['json']['debug'] ?? []);
    }
    $state['selection_source'] = $selectionSource;
    $state['selection_debug'] = is_array($latest) ? $selectionDebug : [];

    if (!$state['authorized']) {
        $state['error'] = station_update_response_reason($authorize);
        if ($state['error'] === '') {
            $state['error'] = station_update_response_reason($check);
        }
        if ($state['error'] === '') {
            $state['error'] = 'domain_not_authorized';
        }
    } elseif (empty($check['ok'])) {
        $state['error'] = station_update_response_reason($check);
        if ($state['error'] === '') {
            $state['error'] = 'check_failed';
        }
    }

    if ($state['error'] === null) {
        station_cache_set($successCacheKey, $state);
        station_cache_delete($failureCacheKey);
    } else {
        station_cache_set($failureCacheKey, $state);
    }

    station_cache_set('station_update_state_debug', $state);
    $runtime = $state;
    return $runtime;
}

function station_update_debug_snapshot(bool $forceRefresh = false): array
{
    $domain = station_current_domain();
    $currentVersion = trim((string)(app_config()['app_version'] ?? ''));
    $api = station_update_server();
    $scope = md5($domain . '|' . $currentVersion . '|' . $api);

    return [
        'update_api_url' => $api,
        'domain' => $domain,
        'current_version' => $currentVersion,
        'success_cache' => station_cache_get('station_update_state_ok_v1_' . $scope, 86400),
        'failure_cache' => station_cache_get('station_update_state_fail_v1_' . $scope, 86400),
        'last_request' => station_cache_get('station_update_debug_last', 86400),
        'state' => fetchUpdateState($forceRefresh),
    ];
}

function station_cache_file(string $key): string
{
    $safe = preg_replace('/[^A-Za-z0-9_.-]/', '_', $key);
    return __DIR__ . '/../storage/cache/' . $safe . '.json';
}

function station_cache_get(string $key, int $maxAge = 0): ?array
{
    $file = station_cache_file($key);
    if (!is_file($file)) {
        return null;
    }

    $raw = @file_get_contents($file);
    if ($raw === false || $raw === '') {
        return null;
    }

    $decoded = json_decode($raw, true);
    if (!is_array($decoded) || !isset($decoded['saved_at']) || !isset($decoded['data']) || !is_array($decoded['data'])) {
        return null;
    }

    if ($maxAge > 0 && (time() - (int)$decoded['saved_at']) > $maxAge) {
        return null;
    }

    return (array)$decoded['data'];
}

function station_cache_set(string $key, array $data): void
{
    $file = station_cache_file($key);
    $dir = dirname($file);
    app_safe_mkdir($dir, 0755);

    $payload = json_encode([
        'saved_at' => time(),
        'data' => $data,
    ], JSON_UNESCAPED_UNICODE);

    if ($payload === false) {
        return;
    }

    @file_put_contents($file, $payload, LOCK_EX);
}

function station_cache_delete(string $key): void
{
    $file = station_cache_file($key);
    if (is_file($file)) {
        @unlink($file);
    }
}

function station_secure_cache_key(): string
{
    app_ensure_bridge_security_runtime();
    $cfg = app_security_config();
    $key = trim((string)($cfg['local']['cache_key'] ?? ''));
    if ($key !== '') {
        return $key;
    }

    return app_derived_secret('bridge_cache');
}

function station_secure_cache_get(string $key, int $maxAge = 0): ?array
{
    $file = station_cache_file($key);
    if (!is_file($file)) {
        return null;
    }

    $raw = @file_get_contents($file);
    if (!is_string($raw) || $raw === '') {
        return null;
    }

    $decoded = json_decode($raw, true);
    if (!is_array($decoded) || !isset($decoded['saved_at'], $decoded['payload'], $decoded['mac']) || !is_array($decoded['payload'])) {
        return null;
    }

    $mac = (string)$decoded['mac'];
    $unsigned = $decoded;
    unset($unsigned['mac']);

    try {
        $calc = hash_hmac('sha256', app_canonical_json($unsigned), station_secure_cache_key());
    } catch (Throwable $e) {
        return null;
    }

    if (!hash_equals($calc, $mac)) {
        return null;
    }
    if ($maxAge > 0 && (time() - (int)$decoded['saved_at']) > $maxAge) {
        return null;
    }

    return (array)$decoded['payload'];
}

function station_secure_cache_set(string $key, array $payload): void
{
    $file = station_cache_file($key);
    $dir = dirname($file);
    app_safe_mkdir($dir, 0755);

    $entry = [
        'saved_at' => time(),
        'payload' => $payload,
    ];

    try {
        $entry['mac'] = hash_hmac('sha256', app_canonical_json($entry), station_secure_cache_key());
        $json = app_canonical_json($entry);
    } catch (Throwable $e) {
        return;
    }

    @file_put_contents($file, $json, LOCK_EX);
}

function station_cache_bust_token(): string
{
    try {
        return bin2hex(random_bytes(6));
    } catch (Throwable $e) {
        return str_replace('.', '', sprintf('%.6F', microtime(true)));
    }
}

function station_header_pick(array $headers, string $name): string
{
    $name = strtolower(trim($name));
    foreach ($headers as $line) {
        $line = (string)$line;
        $pos = strpos($line, ':');
        if ($pos === false) {
            continue;
        }
        $k = strtolower(trim(substr($line, 0, $pos)));
        if ($k === $name) {
            return trim(substr($line, $pos + 1));
        }
    }
    return '';
}

function station_bridge_headers(string $domain, string $currentVersion = '', string $target = 'online'): array
{
    $domain = trim($domain);
    $headers = [];
    if ($domain !== '') {
        $headers[] = 'X-Client-Domain: ' . $domain;
        $headers[] = 'X-Domain: ' . $domain;
    }

    $currentVersion = trim($currentVersion);
    if ($currentVersion !== '') {
        $headers[] = 'X-Client-Version: ' . $currentVersion;
        $headers[] = 'X-App-Version: ' . $currentVersion;
        $headers[] = 'X-Update-Version: ' . $currentVersion;
    }

    $target = trim($target);
    if ($target !== '') {
        $headers[] = 'X-Delivery-Target: ' . $target;
        $headers[] = 'X-Client-Target: ' . $target;
        $headers[] = 'X-Update-Target: ' . $target;
    }

    $token = function_exists('station_bridge_token') ? trim((string)station_bridge_token()) : '';
    if ($token !== '') {
        $headers[] = 'X-API-KEY: ' . $token;
        $headers[] = 'X-API-TOKEN: ' . $token;
        $headers[] = 'X-Update-Token: ' . $token;
        $headers[] = 'X-Auth-Station-Token: ' . $token;
        $headers[] = 'Authorization: Bearer ' . $token;
    }

    return array_values(array_unique($headers));
}

function station_api_get_json(string $url, array $headers = [], int $timeout = 15, int $retries = 2, int $connectTimeout = 6): array
{
    $timeout = max(3, $timeout);
    $connectTimeout = max(2, min($timeout - 1, $connectTimeout));
    $attempts = max(1, $retries + 1);
    $last = ['status' => 0, 'body' => '', 'error' => ''];

    $shouldRetry = static function (array $res): bool {
        $status = (int)($res['status'] ?? 0);
        $error = trim((string)($res['error'] ?? ''));
        return $status === 0 || $status >= 500 || $error !== '';
    };

    if (function_exists('station_bridge_request_signature_headers_for_request')) {
        $signedHeaders = station_bridge_request_signature_headers_for_request('GET', $url, station_current_domain(), '');
        if (is_array($signedHeaders) && $signedHeaders) {
            $headers = array_merge($headers, $signedHeaders);
        }
    }
    $headers = array_values(array_unique($headers));

    if (function_exists('curl_init')) {
        $fetch = static function () use ($url, $headers, $timeout, $connectTimeout): array {
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_TIMEOUT => $timeout,
                CURLOPT_CONNECTTIMEOUT => $connectTimeout,
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_SSL_VERIFYHOST => 2,
                CURLOPT_NOSIGNAL => true,
            ]);
            $resp = curl_exec($ch);
            $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $err = curl_error($ch) ?: '';
            curl_close($ch);
            return ['status' => $status, 'body' => $resp === false ? '' : (string)$resp, 'error' => $err];
        };
    } else {
        $fetch = static function () use ($url, $headers, $timeout): array {
            $context = stream_context_create([
                'http' => [
                    'method' => 'GET',
                    'header' => implode("\r\n", $headers),
                    'timeout' => $timeout,
                ],
                'ssl' => [
                    'verify_peer' => true,
                    'verify_peer_name' => true,
                    'allow_self_signed' => false,
                ],
            ]);
            $resp = @file_get_contents($url, false, $context);
            $status = 0;
            $error = '';
            if ($resp === false) {
                $error = 'stream_request_failed';
            }
            if (!empty($http_response_header[0]) && preg_match('/\\s(\\d{3})\\s/', $http_response_header[0], $m)) {
                $status = (int)$m[1];
            }
            return [
                'status' => $status,
                'body' => $resp === false ? '' : (string)$resp,
                'error' => $error,
            ];
        };
    }

    for ($attempt = 0; $attempt < $attempts; $attempt++) {
        $last = $fetch();
        if ($attempt + 1 >= $attempts || !$shouldRetry($last)) {
            break;
        }
        usleep(200000 * ($attempt + 1));
    }

    $status = (int)($last['status'] ?? 0);
    $body = (string)($last['body'] ?? '');
    $error = (string)($last['error'] ?? '');
    $json = json_decode($body, true);
    if (!is_array($json)) {
        if ($status > 0 && $body !== '') {
            app_log_api_event('bridge_http_get_non_json', [
                'url' => $url,
                'status' => $status,
            ]);
        }
        $json = [];
    }

    if ($status > 0 && is_array($json) && !empty($json)) {
        $parsed = parse_url($url);
        $query = [];
        if (is_array($parsed) && !empty($parsed['query'])) {
            parse_str((string)$parsed['query'], $query);
            if (!is_array($query)) {
                $query = [];
            }
        }

        $action = strtolower(trim((string)($query['action'] ?? '')));
        if ($action === '') {
            $action = 'check';
        }

        if (in_array($action, ['check', 'authorize', 'public_info'], true) && function_exists('station_bridge_verify_signed_payload')) {
            $domain = trim(station_header_pick($headers, 'x-client-domain'));
            if ($domain === '') {
                $domain = trim((string)($query['domain'] ?? station_current_domain()));
            }
            if ($domain !== '' && !station_bridge_verify_signed_payload($json, $action, $domain)) {
                $requireSignature = function_exists('station_bridge_require_signature') ? station_bridge_require_signature() : false;
                if ($requireSignature) {
                    return [
                        'status' => 498,
                        'body' => $body,
                        'error' => $error,
                        'json' => [
                            'status' => false,
                            'reason' => 'signature_verify_failed',
                        ],
                    ];
                }
                $json['_bridge_verify_failed'] = true;
            }
        }
    }

    return ['status' => $status, 'body' => $body, 'error' => $error, 'json' => $json];
}

function station_connectivity_reason(array $res): string
{
    $status = (int)($res['status'] ?? 0);
    $json = (array)($res['json'] ?? []);
    $error = trim((string)($res['error'] ?? ''));

    if ($status === 0) {
        return $error !== '' ? ('network_error: ' . $error) : 'network_unreachable';
    }
    if ($status === 498) {
        return 'signature_verify_failed';
    }

    $reason = trim((string)($json['reason'] ?? ''));
    if ($reason !== '') {
        return $reason;
    }

    if ($status !== 200) {
        return 'http_' . $status;
    }
    if (empty($json['status'])) {
        return 'status_false';
    }

    return 'ok';
}

function station_connectivity_self_test(bool $forceRefresh = true): array
{
    $state = fetchUpdateState($forceRefresh);
    $trace = is_array($state['request_trace'] ?? null) ? $state['request_trace'] : [];
    $reachable = false;
    foreach ($trace as $item) {
        if ((int)($item['status'] ?? 0) > 0) {
            $reachable = true;
            break;
        }
    }

    return [
        'checked_at' => (string)($state['checked_at'] ?? date('Y-m-d H:i:s')),
        'domain' => station_current_domain(),
        'ok' => empty($state['error']),
        'reachable' => $reachable,
        'authorized' => !empty($state['authorized']),
        'update_api_ok' => empty($state['error']),
        'items' => $trace,
        'message' => empty($state['error']) ? 'ok' : (string)$state['error'],
    ];
}

function station_release_note_items($raw): array {
    $items = [];
    $seen = [];

    $cleanLine = static function (string $line): string {
        $line = trim($line);
        if ($line === '') {
            return '';
        }

        $line = (string)preg_replace('/^[\s\-\*\x{2022}\x{25CF}\x{30FB}\x{00B7}\x{2023}\x{2219}]+/u', '', $line);
        $line = (string)preg_replace('/^[\(\[]?\d{1,3}[\)\]]?[.\)\]:\-]\s*/u', '', $line);
        $line = (string)preg_replace('/^[A-Za-z][.\)]\s*/u', '', $line);
        $line = (string)preg_replace('/^(?:update|optimize|optimization|fix|fixed|feature)\s*[:\-]\s*/iu', '', $line);
        $line = trim($line, " \t\n\r\0\x0B-_*.,;:|");
        $line = (string)preg_replace('/\s+/u', ' ', $line);
        $line = trim($line);
        if ($line === '') {
            return '';
        }

        if (function_exists('mb_substr')) {
            $line = mb_substr($line, 0, 220, 'UTF-8');
        } else {
            $line = substr($line, 0, 220);
        }
        return trim((string)$line);
    };

    $append = static function ($value) use (&$append, &$items, &$seen, $cleanLine): void {
        if (is_array($value)) {
            foreach ($value as $v) {
                $append($v);
            }
            return;
        }
        if (is_object($value)) {
            foreach ((array)$value as $v) {
                $append($v);
            }
            return;
        }
        if (is_bool($value) || $value === null) {
            return;
        }

        $line = $cleanLine((string)$value);
        if ($line === '') {
            return;
        }
        $key = strtolower((string)preg_replace('/[\s\p{P}]+/u', '', $line));
        if ($key === '') {
            $key = strtolower($line);
        }
        if (isset($seen[$key])) {
            return;
        }
        $seen[$key] = true;
        $items[] = $line;
    };

    if (is_array($raw) || is_object($raw)) {
        $append($raw);
        return array_slice($items, 0, 100);
    }

    $text = (string)$raw;
    $text = str_replace(["\r\n", "\r"], "\n", $text);
    $text = str_replace(["\\r\\n", "\\n", "\\r", "\\t"], ["\n", "\n", "\n", ' '], $text);
    $text = (string)preg_replace('/<\s*br\s*\/?>/iu', "\n", $text);
    $text = (string)preg_replace('/<\s*\/\s*li\s*>/iu', "\n", $text);
    $text = (string)preg_replace('/<\s*li\b[^>]*>/iu', '- ', $text);
    $text = strip_tags($text);
    $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $text = trim($text);
    if ($text === '') {
        return [];
    }

    if (
        (substr($text, 0, 1) === '[' && substr($text, -1) === ']')
        || (substr($text, 0, 1) === '{' && substr($text, -1) === '}')
    ) {
        $decoded = json_decode($text, true);
        if (is_array($decoded)) {
            $append($decoded);
            return array_slice($items, 0, 100);
        }
    }

    $text = (string)preg_replace('/[;|\x{FF1B}\x{FF5C}\x{3001}]+/u', "\n", $text);
    $text = (string)preg_replace('/([,\x{FF0C}])(?=\s*(?:[\(\[]?\d{1,3}[\)\]]?[.\)\]:\-]|[A-Za-z][.\)]))/u', "\n", $text);
    $lines = preg_split('/\n+/', $text);
    if (!is_array($lines)) {
        $lines = [$text];
    }
    foreach ($lines as $line) {
        $append((string)$line);
    }

    return array_slice($items, 0, 100);
}

function station_release_note_text($raw): string {
    $items = station_release_note_items($raw);
    if (!$items) {
        return '';
    }
    return trim(implode("\n", $items));
}

function station_front_release_channel_normalize(string $channel): string
{
    $channel = strtolower(trim($channel));
    if ($channel === 'beta' || $channel === 'test' || $channel === 'testing') {
        return 'beta';
    }
    if (in_array($channel, ['free', 'trial', 'demo', 'lite', 'basic'], true)) {
        return 'free';
    }
    if (in_array($channel, ['paid', 'stable', 'normal', 'formal', 'prod', 'production'], true)) {
        return 'paid';
    }
    return '';
}

function station_front_release_channel_label(string $channel): string
{
    return match (station_front_release_channel_normalize($channel)) {
        'free' => app_zh('免费版通道'),
        'beta' => app_zh('测试版通道'),
        'paid' => app_zh('付费版通道'),
        default => '',
    };
}

function remote_notice_data(bool $forceRefresh = false): array {
    static $runtimeCache = null;

    if (!$forceRefresh && is_array($runtimeCache)) {
        return $runtimeCache;
    }

    $state = fetchUpdateState($forceRefresh);
    $site = is_array($state['site_notice'] ?? null) ? (array)$state['site_notice'] : [];
    $latest = is_array($state['latest'] ?? null) ? (array)$state['latest'] : [];
    $currentVersion = trim((string)($state['current_version'] ?? (app_config()['app_version'] ?? '')));

    $siteData = null;
    if (!empty($site['enabled']) || trim((string)($site['title'] ?? '')) !== '' || trim((string)($site['content'] ?? '')) !== '') {
        $title = trim((string)($site['title'] ?? ''));
        $content = trim((string)($site['content'] ?? ''));
        if ($title !== '' || $content !== '') {
            $siteData = [
                'title' => $title,
                'content' => $content,
                'updated_at' => (string)($site['updated_at'] ?? ''),
            ];
        }
    }

    $updateData = null;
    if ($latest) {
        $version = trim((string)($latest['version'] ?? ''));
        $fix = station_release_note_text($latest['fix_note'] ?? ($latest['fix_items'] ?? ''));
        $opt = station_release_note_text($latest['optimize_note'] ?? ($latest['optimize_items'] ?? ''));
        $upd = station_release_note_text($latest['update_note'] ?? ($latest['update_items'] ?? ''));
        $isNewerVersion = !empty($state['has_update']) && $version !== '' && station_is_version_newer($version, $currentVersion);
        if ($isNewerVersion || (($fix !== '' || $opt !== '' || $upd !== '') && $version === '')) {
            $updateData = [
                'version' => $version,
                'created_at' => (string)($latest['created_at'] ?? ''),
                'channel' => trim((string)($latest['release_channel'] ?? ($latest['channel'] ?? ''))),
            ];
            if ($upd !== '') {
                $updateData['update_note'] = $upd;
            }
            if ($opt !== '') {
                $updateData['optimize_note'] = $opt;
            }
            if ($fix !== '') {
                $updateData['fix_note'] = $fix;
            }
        }
    }

    $runtimeCache = ['site_notice' => $siteData, 'update_notice' => $updateData];
    return $runtimeCache;
}

function station_backend_authorization_status(bool $forceRefresh = false): array
{
    static $cache = null;
    static $cachedAt = 0;

    if (!$forceRefresh && is_array($cache) && (time() - $cachedAt) < 30) {
        return $cache;
    }

    $cache = [
        'authorized' => true,
        'reachable' => true,
        'reason' => 'open_source_mode',
        'prompt_title' => '',
        'prompt_content' => '',
        'license_id' => 'open-source',
        'offline_until' => time() + 315360000,
    ];
    $cachedAt = time();
    return $cache;
}

function station_backend_block_message(bool $forceRefresh = false): ?array
{
    return null;
}
