<?php
function ugfx_read_json(string $path, $default = []) {
    if (!file_exists($path)) return $default;
    $data = json_decode(file_get_contents($path), true);
    return is_array($data) ? $data : $default;
}

function ugfx_write_json(string $path, $data): bool {
    $dir = dirname($path);
    if (!is_dir($dir)) mkdir($dir, 0775, true);
    return (bool) file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}

function ugfx_get_settings(): array {
    static $settings = null;
    if ($settings !== null) return $settings;
    $path = dirname(__DIR__) . '/data/settings.json';
    $settings = ugfx_read_json($path, []);
    return is_array($settings) ? $settings : [];
}

function ugfx_save_settings(array $settings): bool {
    $path = dirname(__DIR__) . '/data/settings.json';
    return ugfx_write_json($path, $settings);
}

function ugfx_chat_enabled(): bool {
    $settings = ugfx_get_settings();
    return !array_key_exists('chat_enabled', $settings) || !empty($settings['chat_enabled']);
}

function ugfx_chat_public_status(): array {
    if (ugfx_chat_enabled()) {
        return ['enabled' => true, 'message' => ''];
    }
    $settings = ugfx_get_settings();
    $message = trim((string)($settings['chat_disabled_message'] ?? 'Chat is currently turned off by the station team. Please check back later.'));
    return ['enabled' => false, 'message' => $message ?: 'Chat is currently turned off by the station team. Please check back later.'];
}

function ugfx_file_is_safe_upload(array $file, array $allowedMimeStarts = ['image/', 'audio/']): bool {
    $tmp = (string)($file['tmp_name'] ?? '');
    if ($tmp === '' || !is_file($tmp)) return false;
    if (!function_exists('finfo_open')) return true;
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    if (!$finfo) return true;
    $mime = (string)finfo_file($finfo, $tmp);
    finfo_close($finfo);
    foreach ($allowedMimeStarts as $allowed) {
        if (str_starts_with($mime, $allowed)) return true;
    }
    return false;
}

function ugfx_csrf_token(): string {
    if (empty($_SESSION['ugfx_csrf']) || !is_string($_SESSION['ugfx_csrf'])) {
        $_SESSION['ugfx_csrf'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['ugfx_csrf'];
}

function ugfx_verify_csrf(?string $token): bool {
    $sessionToken = (string)($_SESSION['ugfx_csrf'] ?? '');
    $token = trim((string)$token);
    return $sessionToken !== '' && $token !== '' && hash_equals($sessionToken, $token);
}

function ugfx_verify_csrf_or_die(): void {
    $token = (string)($_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
    if (ugfx_verify_csrf($token)) return;
    http_response_code(419);
    echo '<div style="padding:24px;font-family:Inter,Arial,sans-serif;color:white;background:#111418;min-height:100vh">Security check failed. Please refresh the page and try again.</div>';
    exit;
}

function ugfx_admin_log(string $action, array $context = []): void {
    $path = dirname(__DIR__) . '/storage/logs/admin-actions.log';
    $dir = dirname($path);
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    $line = [
        'time' => date('c'),
        'user' => (string)((ugfx_admin_current_user()['username'] ?? 'guest')),
        'action' => $action,
        'ip' => (string)($_SERVER['REMOTE_ADDR'] ?? ''),
        'context' => $context,
    ];
    @file_put_contents($path, json_encode($line, JSON_UNESCAPED_SLASHES) . PHP_EOL, FILE_APPEND | LOCK_EX);
}

function ugfx_protect_storage_dir(string $dir): void {
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    $index = rtrim($dir, '/\\') . '/index.html';
    if (!is_file($index)) @file_put_contents($index, '');
    $htaccess = rtrim($dir, '/\\') . '/.htaccess';
    if (!is_file($htaccess)) {
        $rules = <<<'HTACCESS'
Options -Indexes
<FilesMatch "\.(php|phtml|phar|cgi|pl|py|sh)$">
Deny from all
</FilesMatch>
HTACCESS;
        @file_put_contents($htaccess, $rules);
    }
}

function ugfx_cleanup_chat_uploads(int $maxAgeDays = 30): void {
    $dirs = [dirname(__DIR__) . '/storage/chat', dirname(__DIR__) . '/storage/uploads'];
    $cutoff = time() - max(1, $maxAgeDays) * 86400;
    foreach ($dirs as $dir) {
        if (!is_dir($dir)) continue;
        foreach (glob($dir . '/*') ?: [] as $file) {
            if (!is_file($file)) continue;
            $base = basename($file);
            if ($base === 'index.html' || $base === '.htaccess') continue;
            if (@filemtime($file) !== false && @filemtime($file) < $cutoff) {
                @unlink($file);
            }
        }
    }
}


function ugfx_admin_users(): array {
    global $ugfx;
    $users = ugfx_read_json($ugfx['storage']['users'] ?? '', []);
    return is_array($users) ? array_values(array_filter($users, 'is_array')) : [];
}

function ugfx_admin_current_user(): ?array {
    $user = $_SESSION['ugfx_admin'] ?? null;
    return is_array($user) ? $user : null;
}


function ugfx_admin_save_users(array $users): bool {
    global $ugfx;
    return ugfx_write_json($ugfx['storage']['users'] ?? '', array_values($users));
}

function ugfx_admin_update_user_meta(string $username, array $meta): void {
    $username = trim($username);
    if ($username === '') return;
    $items = ugfx_admin_users();
    $changed = false;
    foreach ($items as &$item) {
        if (strcasecmp((string)($item['username'] ?? ''), $username) !== 0) continue;
        foreach ($meta as $k => $v) {
            $item[$k] = $v;
        }
        $changed = true;
        break;
    }
    unset($item);
    if ($changed) ugfx_admin_save_users($items);
}

function ugfx_admin_touch_current_user(): void {
    $user = ugfx_admin_current_user();
    if (!$user || empty($user['username'])) return;
    $now = date('Y-m-d H:i:s');
    ugfx_admin_update_user_meta((string)$user['username'], ['last_seen' => $now]);
    $_SESSION['ugfx_admin']['last_seen'] = $now;
}

function ugfx_admin_online_users(int $seconds = 300): array {
    $threshold = time() - $seconds;
    return array_values(array_filter(ugfx_admin_users(), function($item) use ($threshold) {
        $seen = strtotime((string)($item['last_seen'] ?? ''));
        return $seen && $seen >= $threshold;
    }));
}

function ugfx_admin_logged_in(): bool {
    return ugfx_admin_current_user() !== null;
}

function ugfx_admin_attempt(string $username, string $password): bool {
    $username = trim($username);
    $password = (string)$password;
    if ($username === '' || $password === '') {
        return false;
    }

    foreach (ugfx_admin_users() as $user) {
        $storedUser = trim((string)($user['username'] ?? ''));
        if ($storedUser === '' || strcasecmp($storedUser, $username) !== 0) {
            continue;
        }

        $storedPassword = (string)($user['password'] ?? '');
        $valid = false;
        if ($storedPassword !== '') {
            if (password_get_info($storedPassword)['algo'] !== null) {
                $valid = password_verify($password, $storedPassword);
            } else {
                $valid = hash_equals($storedPassword, $password);
            }
        }

        if (!$valid) {
            return false;
        }

        $now = date('Y-m-d H:i:s');
        $_SESSION['ugfx_admin'] = [
            'username' => $storedUser,
            'role' => trim((string)($user['role'] ?? 'admin')) ?: 'admin',
            'permissions' => is_array($user['permissions'] ?? null) ? array_values($user['permissions']) : [],
            'temp_login' => !empty($user['temp_login']),
            'logged_in_at' => time(),
            'last_login' => $now,
            'last_seen' => $now,
        ];
        ugfx_admin_update_user_meta($storedUser, ['last_login' => $now, 'last_seen' => $now]);
        session_regenerate_id(true);
        return true;
    }

    return false;
}


function ugfx_normalize_role(string $role): string {
    $role = strtolower(trim($role));
    return $role !== '' ? $role : 'admin';
}

function ugfx_admin_role_rank(string $role): int {
    return match (ugfx_normalize_role($role)) {
        'superadmin' => 100,
        'admin' => 80,
        'editor' => 50,
        'dj' => 30,
        default => 10,
    };
}

function ugfx_admin_permission_defaults(string $role): array {
    $role = ugfx_normalize_role($role);
    return match ($role) {
        'superadmin' => ['dashboard','chat','news','events','pages','friends','djs','schedule','charts','requests','contact','applications','users','media'],
        'admin' => ['dashboard','chat','news','events','pages','friends','djs','schedule','charts','requests','contact','applications','media'],
        'editor' => ['dashboard','chat','news','events','pages','friends','djs','charts','media'],
        'dj' => ['dashboard','chat'],
        default => ['dashboard'],
    };
}

function ugfx_admin_permissions_for_user(?array $user = null): array {
    $user = $user ?: ugfx_admin_current_user();
    if (!$user) return [];
    $permissions = $user['permissions'] ?? ugfx_admin_permission_defaults((string)($user['role'] ?? 'admin'));
    if (!is_array($permissions) || !$permissions) {
        $permissions = ugfx_admin_permission_defaults((string)($user['role'] ?? 'admin'));
    }
    $permissions = array_values(array_unique(array_filter(array_map(fn($v) => strtolower(trim((string)$v)), $permissions))));
    if (!in_array('dashboard', $permissions, true)) $permissions[] = 'dashboard';
    return $permissions;
}

function ugfx_admin_can(string $permission, ?array $user = null): bool {
    $user = $user ?: ugfx_admin_current_user();
    if (!$user) return false;
    if (ugfx_admin_role_rank((string)($user['role'] ?? '')) >= 100) return true;
    return in_array(strtolower($permission), ugfx_admin_permissions_for_user($user), true);
}

function ugfx_require_permission(string $permission): void {
    ugfx_require_admin();
    if (ugfx_admin_can($permission)) return;
    http_response_code(403);
    echo '<div style="padding:24px;font-family:Inter,Arial,sans-serif;color:white;background:#111418;min-height:100vh">Access denied for this admin section.</div>';
    exit;
}

function ugfx_admin_all_permissions(): array {
    return [
        'dashboard' => 'Dashboard',
        'chat' => 'Chat',
        'news' => 'News',
        'events' => 'Events',
        'pages' => 'Pages',
        'friends' => 'Friends',
        'djs' => 'DJs / Team',
        'schedule' => 'Schedule',
        'charts' => 'Charts',
        'requests' => 'Requests',
        'contact' => 'Contact',
        'applications' => 'Applications',
        'users' => 'Users & Roles',
        'media' => 'Media uploads',
    ];
}

function ugfx_upload_file(array $file, array $allowedExts = ['jpg','jpeg','png','gif','webp'], int $maxBytes = 6291456): ?string {
    global $ugfx;
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null;
    if (($file['size'] ?? 0) <= 0 || ($file['size'] ?? 0) > $maxBytes) return null;
    if (!ugfx_file_is_safe_upload($file)) return null;
    $ext = strtolower(pathinfo((string)($file['name'] ?? ''), PATHINFO_EXTENSION));
    if (!in_array($ext, $allowedExts, true)) return null;
    $dir = dirname($ugfx['storage']['chat_uploads']) . '/uploads';
    ugfx_protect_storage_dir($dir);
    $base = ugfx_slugify(pathinfo((string)($file['name'] ?? 'upload'), PATHINFO_FILENAME));
    $name = $base . '-' . date('YmdHis') . '-' . bin2hex(random_bytes(3)) . '.' . $ext;
    $target = $dir . '/' . $name;
    if (!move_uploaded_file($file['tmp_name'], $target)) return null;
    return 'storage/uploads/' . $name;
}

function ugfx_chat_ban_data(): array {
    global $ugfx;
    $data = ugfx_read_json($ugfx['storage']['chat_bans'] ?? '', ['banned' => [], 'muted' => []]);
    if (!is_array($data)) $data = ['banned' => [], 'muted' => []];
    $data['banned'] = array_values(array_unique(array_filter(array_map('trim', (array)($data['banned'] ?? [])))));
    $data['muted'] = array_values(array_unique(array_filter(array_map('trim', (array)($data['muted'] ?? [])))));
    return $data;
}

function ugfx_save_chat_ban_data(array $data): bool {
    global $ugfx;
    $clean = [
        'banned' => array_values(array_unique(array_filter(array_map('trim', (array)($data['banned'] ?? []))))),
        'muted' => array_values(array_unique(array_filter(array_map('trim', (array)($data['muted'] ?? []))))),
    ];
    return ugfx_write_json($ugfx['storage']['chat_bans'], $clean);
}

function ugfx_chat_name_state(string $name): string {
    $name = trim($name);
    if ($name === '') return 'ok';
    $data = ugfx_chat_ban_data();
    foreach ((array)$data['banned'] as $entry) if (strcasecmp((string)$entry, $name) === 0) return 'banned';
    foreach ((array)$data['muted'] as $entry) if (strcasecmp((string)$entry, $name) === 0) return 'muted';
    return 'ok';
}

function ugfx_require_admin(): void {
    if (ugfx_admin_logged_in()) {
        return;
    }
    if (!headers_sent()) {
        header('Location: login.php');
    }
    exit;
}

function ugfx_asset(string $path): string {
    return 'assets/' . ltrim($path, '/');
}

function ugfx_vendor_asset(string $localPath, string $cdnUrl, string $prefix = ''): string {
    $normalized = ltrim($localPath, '/');
    $absolute = dirname(__DIR__) . '/' . $normalized;
    if (file_exists($absolute)) {
        return $prefix . $normalized;
    }
    return $cdnUrl;
}

function ugfx_is_active(string $page, string $current): string {
    return $page === $current ? 'active' : '';
}

function ugfx_slugify(string $text): string {
    $text = strtolower(trim($text));
    $text = preg_replace('/[^a-z0-9]+/', '-', $text);
    return trim($text ?? '', '-') ?: 'page';
}

function ugfx_strlen(string $text): int {
    return function_exists('mb_strlen') ? mb_strlen($text) : strlen($text);
}

function ugfx_substr(string $text, int $start, ?int $length = null): string {
    if (function_exists('mb_substr')) {
        return $length === null ? mb_substr($text, $start) : mb_substr($text, $start, $length);
    }
    return $length === null ? substr($text, $start) : substr($text, $start, $length);
}

function ugfx_excerpt(string $text, int $length = 180): string {
    $text = trim(strip_tags($text));
    return ugfx_strlen($text) > $length ? ugfx_substr($text, 0, $length - 1) . '…' : $text;
}

function ugfx_sort_items_by_date_desc(array $items): array {
    usort($items, function ($a, $b) {
        return strtotime((string)($b['start'] ?? $b['date'] ?? '')) <=> strtotime((string)($a['start'] ?? $a['date'] ?? ''));
    });
    return $items;
}

function ugfx_get_news(int $limit = 6): array {
    global $ugfx;
    $items = ugfx_read_json($ugfx['storage']['news'], []);
    $items = array_values(array_filter($items, function ($item) { return ($item['published'] ?? 0) == 1; }));
    return array_slice($items, 0, $limit);
}



function ugfx_top50_normalize_item(array $item, string $fallbackTitle = ''): array {
    $title = trim((string)($item['title'] ?? $fallbackTitle));
    if ($title === '') {
        $title = 'Untitled Track';
    }
    $count = max(0, (int)($item['count'] ?? $item['plays'] ?? 0));
    $firstSeen = trim((string)($item['first_seen'] ?? ''));
    $lastSeen = trim((string)($item['last_seen'] ?? $item['updated_at'] ?? $item['first_seen'] ?? ''));
    $artist = trim((string)($item['artist'] ?? ''));
    $song = trim((string)($item['song'] ?? ''));
    if (($artist === '' || $song === '') && strpos($title, ' - ') !== false) {
        [$maybeArtist, $maybeSong] = explode(' - ', $title, 2);
        if ($artist === '') $artist = trim($maybeArtist);
        if ($song === '') $song = trim($maybeSong);
    }
    return [
        'title' => $title,
        'artist' => $artist,
        'song' => $song,
        'count' => $count,
        'first_seen' => $firstSeen,
        'last_seen' => $lastSeen,
        'updated_at' => trim((string)($item['updated_at'] ?? $lastSeen ?: $firstSeen)),
        'source' => trim((string)($item['source'] ?? 'manual')),
        'notes' => trim((string)($item['notes'] ?? '')),
    ];
}

function ugfx_top50_all(): array {
    global $ugfx;
    $raw = ugfx_read_json($ugfx['storage']['top50'], []);
    if (!is_array($raw)) {
        return [];
    }
    $items = [];
    foreach ($raw as $key => $value) {
        if (!is_array($value)) {
            continue;
        }
        $items[] = ugfx_top50_normalize_item($value, is_string($key) ? $key : '');
    }
    usort($items, function ($a, $b) {
        $countSort = ((int)($b['count'] ?? 0)) <=> ((int)($a['count'] ?? 0));
        if ($countSort !== 0) return $countSort;
        $timeA = strtotime((string)($a['last_seen'] ?? '')) ?: 0;
        $timeB = strtotime((string)($b['last_seen'] ?? '')) ?: 0;
        $timeSort = $timeB <=> $timeA;
        if ($timeSort !== 0) return $timeSort;
        return strcasecmp((string)($a['title'] ?? ''), (string)($b['title'] ?? ''));
    });
    return array_values($items);
}

function ugfx_save_top50(array $items): bool {
    global $ugfx;
    $normalized = [];
    foreach ($items as $item) {
        if (!is_array($item)) continue;
        $row = ugfx_top50_normalize_item($item, (string)($item['title'] ?? ''));
        if (trim((string)$row['title']) === '') continue;
        $normalized[$row['title']] = [
            'title' => $row['title'],
            'artist' => $row['artist'],
            'song' => $row['song'],
            'count' => (int)$row['count'],
            'first_seen' => $row['first_seen'],
            'last_seen' => $row['last_seen'],
            'updated_at' => $row['updated_at'] ?: date('Y-m-d H:i:s'),
            'source' => $row['source'],
            'notes' => $row['notes'],
        ];
    }
    uasort($normalized, function ($a, $b) {
        $countSort = ((int)($b['count'] ?? 0)) <=> ((int)($a['count'] ?? 0));
        if ($countSort !== 0) return $countSort;
        return strcasecmp((string)($a['title'] ?? ''), (string)($b['title'] ?? ''));
    });
    return ugfx_write_json($ugfx['storage']['top50'], $normalized);
}

function ugfx_top50_stats(): array {
    $items = ugfx_top50_all();
    $totalPlays = 0;
    $artists = [];
    $latest = 0;
    foreach ($items as $item) {
        $totalPlays += (int)($item['count'] ?? 0);
        $artist = trim((string)($item['artist'] ?? ''));
        if ($artist === '' && strpos((string)($item['title'] ?? ''), ' - ') !== false) {
            $artist = trim(explode(' - ', (string)$item['title'], 2)[0]);
        }
        if ($artist !== '') $artists[strtolower($artist)] = $artist;
        $seen = strtotime((string)($item['last_seen'] ?? '')) ?: strtotime((string)($item['updated_at'] ?? '')) ?: 0;
        if ($seen > $latest) $latest = $seen;
    }
    return [
        'tracks' => count($items),
        'total_plays' => $totalPlays,
        'artists' => count($artists),
        'last_updated' => $latest ? date('Y-m-d H:i:s', $latest) : '',
        'top_track' => $items[0] ?? null,
    ];
}

function ugfx_top50_parse_bulk(string $input): array {
    $rows = preg_split('/
|
|
/', trim($input));
    $items = [];
    foreach ($rows as $line) {
        $line = trim($line);
        if ($line === '') continue;
        $parts = preg_split('/\s*\|\s*|\s*	\s*|\s*;\s*/', $line);
        $title = trim((string)($parts[0] ?? ''));
        if ($title === '') continue;
        $count = isset($parts[1]) ? (int)preg_replace('/[^0-9-]/', '', (string)$parts[1]) : 0;
        $lastSeen = trim((string)($parts[2] ?? ''));
        $notes = trim((string)($parts[3] ?? ''));
        $items[] = ugfx_top50_normalize_item([
            'title' => $title,
            'count' => max(0, $count),
            'last_seen' => $lastSeen,
            'updated_at' => date('Y-m-d H:i:s'),
            'source' => 'bulk-import',
            'notes' => $notes,
        ], $title);
    }
    return $items;
}


function ugfx_chart_clean_title(string $title): string {
    $title = html_entity_decode(trim($title), ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $title = preg_replace('/\s+/', ' ', $title ?? '');
    $title = trim((string)$title, " \t\n\r\0\x0B-");
    $junk = ['auto dj', 'autodj', 'live stream', 'unknown', 'n/a', 'offline', 'mountpoint', 'stream'];
    if ($title === '' || in_array(strtolower($title), $junk, true)) {
        return '';
    }
    return trim((string)$title);
}

function ugfx_chart_meta(): array {
    global $ugfx;
    $meta = ugfx_read_json($ugfx['storage']['chart_meta'], []);
    return is_array($meta) ? $meta : [];
}

function ugfx_save_chart_meta(array $meta): bool {
    global $ugfx;
    $meta['updated_at'] = date('Y-m-d H:i:s');
    return ugfx_write_json($ugfx['storage']['chart_meta'], $meta);
}

function ugfx_chart_history(int $limit = 100): array {
    global $ugfx;
    $rows = ugfx_read_json($ugfx['storage']['chart_history'], []);
    if (!is_array($rows)) return [];
    $rows = array_values(array_filter($rows, 'is_array'));
    usort($rows, function ($a, $b) {
        return (strtotime((string)($b['seen_at'] ?? '')) ?: 0) <=> (strtotime((string)($a['seen_at'] ?? '')) ?: 0);
    });
    return array_slice($rows, 0, max(1, $limit));
}

function ugfx_chart_record_auto_hit(string $title, array $context = []): bool {
    global $ugfx;
    $title = ugfx_chart_clean_title($title);
    if ($title === '') return false;

    $seenAt = trim((string)($context['seen_at'] ?? date('Y-m-d H:i:s')));
    $source = trim((string)($context['source'] ?? 'icecast-auto')) ?: 'icecast-auto';
    $mount = trim((string)($context['mount'] ?? ''));
    $listeners = max(0, (int)($context['listeners'] ?? 0));
    $notes = trim((string)($context['notes'] ?? ''));

    $items = ugfx_top50_all();
    $matched = false;
    foreach ($items as &$item) {
        if (strcasecmp((string)($item['title'] ?? ''), $title) === 0) {
            $item['count'] = (int)($item['count'] ?? 0) + 1;
            if (trim((string)($item['first_seen'] ?? '')) === '') $item['first_seen'] = $seenAt;
            $item['last_seen'] = $seenAt;
            $item['updated_at'] = $seenAt;
            $item['source'] = $source;
            if ($notes !== '') $item['notes'] = $notes;
            $matched = true;
            break;
        }
    }
    unset($item);
    if (!$matched) {
        $items[] = ugfx_top50_normalize_item([
            'title' => $title,
            'count' => 1,
            'first_seen' => $seenAt,
            'last_seen' => $seenAt,
            'updated_at' => $seenAt,
            'source' => $source,
            'notes' => $notes,
        ], $title);
    }
    ugfx_save_top50($items);

    $history = ugfx_read_json($ugfx['storage']['chart_history'], []);
    if (!is_array($history)) $history = [];
    $history[] = [
        'title' => $title,
        'seen_at' => $seenAt,
        'source' => $source,
        'mount' => $mount,
        'listeners' => $listeners,
    ];
    if (count($history) > 5000) {
        $history = array_slice($history, -5000);
    }
    ugfx_write_json($ugfx['storage']['chart_history'], array_values($history));
    return true;
}

function ugfx_chart_rebuild_from_history(int $days = 30): array {
    $days = max(1, $days);
    $cutoff = time() - ($days * 86400);
    $history = ugfx_chart_history(5000);
    $counts = [];
    foreach ($history as $row) {
        $title = ugfx_chart_clean_title((string)($row['title'] ?? ''));
        if ($title === '') continue;
        $seenTs = strtotime((string)($row['seen_at'] ?? '')) ?: 0;
        if ($seenTs < $cutoff) continue;
        $key = strtolower($title);
        if (!isset($counts[$key])) {
            $counts[$key] = [
                'title' => $title,
                'count' => 0,
                'first_seen' => (string)($row['seen_at'] ?? ''),
                'last_seen' => (string)($row['seen_at'] ?? ''),
                'updated_at' => (string)($row['seen_at'] ?? ''),
                'source' => trim((string)($row['source'] ?? 'icecast-auto')) ?: 'icecast-auto',
                'notes' => 'Auto rebuilt from live chart history',
            ];
        }
        $counts[$key]['count']++;
        if (($seenTs) < (strtotime((string)$counts[$key]['first_seen']) ?: PHP_INT_MAX)) {
            $counts[$key]['first_seen'] = (string)($row['seen_at'] ?? '');
        }
        if (($seenTs) >= (strtotime((string)$counts[$key]['last_seen']) ?: 0)) {
            $counts[$key]['last_seen'] = (string)($row['seen_at'] ?? '');
            $counts[$key]['updated_at'] = (string)($row['seen_at'] ?? '');
        }
    }
    ugfx_save_top50(array_values($counts));
    $meta = ugfx_chart_meta();
    $meta['last_rebuild_at'] = date('Y-m-d H:i:s');
    $meta['window_days'] = $days;
    ugfx_save_chart_meta($meta);
    return ugfx_top50_stats();
}

function ugfx_chart_autoupdate(bool $force = false): array {
    $meta = ugfx_chart_meta();
    $interval = max(15, (int)($meta['poll_interval_seconds'] ?? 20));
    $lastPoll = strtotime((string)($meta['last_poll_at'] ?? '')) ?: 0;
    if (!$force && $lastPoll && (time() - $lastPoll) < $interval) {
        return ['ok' => true, 'skipped' => true, 'reason' => 'throttled', 'meta' => $meta];
    }

    $status = ugfx_icecast_status();
    $title = ugfx_chart_clean_title((string)($status['title'] ?? ''));
    $mount = trim((string)($status['mount'] ?? ''));
    $listeners = max(0, (int)($status['current_listeners'] ?? $status['server_listeners'] ?? 0));
    $now = date('Y-m-d H:i:s');

    $meta['last_poll_at'] = $now;
    $meta['last_mount'] = $mount;
    $meta['last_seen_title'] = $title;
    $meta['last_seen_listeners'] = $listeners;

    if ($title === '') {
        ugfx_save_chart_meta($meta);
        return ['ok' => false, 'recorded' => false, 'reason' => 'no_current_track', 'meta' => $meta];
    }

    $lastRecordedTitle = ugfx_chart_clean_title((string)($meta['last_recorded_title'] ?? ''));
    $recorded = false;
    if ($force || strcasecmp($title, $lastRecordedTitle) !== 0) {
        $recorded = ugfx_chart_record_auto_hit($title, [
            'seen_at' => $now,
            'source' => 'icecast-auto',
            'mount' => $mount,
            'listeners' => $listeners,
            'notes' => 'Auto captured from live Icecast metadata',
        ]);
        if ($recorded) {
            $meta['last_recorded_title'] = $title;
            $meta['last_recorded_at'] = $now;
            $meta['recorded_count'] = (int)($meta['recorded_count'] ?? 0) + 1;
        }
    }

    ugfx_save_chart_meta($meta);
    return [
        'ok' => true,
        'recorded' => $recorded,
        'title' => $title,
        'mount' => $mount,
        'listeners' => $listeners,
        'meta' => $meta,
    ];
}

function ugfx_get_events(int $limit = 4): array {
    global $ugfx;
    $items = ugfx_read_json($ugfx['storage']['events'], []);
    $items = array_values(array_filter($items, function ($item) { return ($item['published'] ?? 0) == 1; }));
    $items = ugfx_sort_items_by_date_desc($items);
    return array_slice($items, 0, $limit);
}

function ugfx_get_djs(int $limit = 8): array {
    global $ugfx;
    $items = ugfx_read_json($ugfx['storage']['djs'], []);
    $items = array_values(array_filter($items, function ($item) {
        return !isset($item['active']) || !empty($item['active']);
    }));
    usort($items, function ($a, $b) {
        $sort = ((int)($a['sort_order'] ?? 9999)) <=> ((int)($b['sort_order'] ?? 9999));
        if ($sort !== 0) return $sort;
        return strcmp((string)($a['name'] ?? ''), (string)($b['name'] ?? ''));
    });
    return array_slice($items, 0, $limit);
}

function ugfx_get_top50(int $limit = 10): array {
    return array_slice(ugfx_top50_all(), 0, max(1, $limit));
}

function ugfx_get_schedule(): array {
    global $ugfx;
    return ugfx_read_json($ugfx['storage']['schedule'], []);
}

function ugfx_get_friends(int $limit = 12): array {
    global $ugfx;
    return array_slice(ugfx_read_json($ugfx['storage']['friends'], []), 0, $limit);
}

function ugfx_get_pages(bool $publishedOnly = true): array {
    global $ugfx;
    $items = ugfx_read_json($ugfx['storage']['pages'], []);
    if ($publishedOnly) {
        $items = array_values(array_filter($items, function ($item) { return ($item['published'] ?? 0) == 1; }));
    }
    return $items;
}

function ugfx_get_page_by_slug(string $slug): ?array {
    foreach (ugfx_get_pages(false) as $page) {
        if (($page['slug'] ?? '') === $slug) {
            return $page;
        }
    }
    return null;
}

function ugfx_day_name(?int $timestamp = null): string {
    return date('l', $timestamp ?? time());
}

function ugfx_current_show(): array {
    $schedule = ugfx_get_schedule();
    $day = ugfx_day_name();
    $hour = (int) date('G');
    $current = $schedule[$day][$hour] ?? 'Underground FX Live';
    $next = $schedule[$day][($hour + 1) % 24] ?? 'Underground FX Live';
    return ['day' => $day, 'hour' => $hour, 'current' => $current, 'next' => $next];
}


function ugfx_merge_global_stats(array ...$sets): array {
    $merged = [
        'listeners' => 0,
        'listener_connections' => 0,
        'clients' => 0,
        'sources' => 0,
        'host' => '',
        'server_id' => '',
        'server_start' => '',
    ];
    foreach ($sets as $set) {
        if (!is_array($set)) continue;
        foreach (['listeners','listener_connections','clients','sources'] as $field) {
            if (isset($set[$field]) && (int)$set[$field] > (int)$merged[$field]) {
                $merged[$field] = (int)$set[$field];
            }
        }
        foreach (['host','server_id','server_start'] as $field) {
            if (($merged[$field] ?? '') === '' && trim((string)($set[$field] ?? '')) !== '') {
                $merged[$field] = trim((string)$set[$field]);
            }
        }
    }
    return $merged;
}

function ugfx_icecast_status(): array {
    static $cached = null;
    if ($cached !== null) {
        return $cached;
    }

    $cfg = ugfx_get_stream_config();
    $playerUrls = ugfx_get_player_urls();
    $cacheFile = __DIR__ . '/../data/icecast_stats_cache.json';
    $cacheTtl = 15;

    if (is_file($cacheFile) && (time() - (int)filemtime($cacheFile) <= $cacheTtl)) {
        $cachedData = json_decode((string)@file_get_contents($cacheFile), true);
        $cacheHasUsefulData = is_array($cachedData)
            && isset($cachedData['server_listeners'])
            && (
                (int)($cachedData['source_count'] ?? 0) > 0
                || !empty($cachedData['public_status_ok'])
                || !empty($cachedData['admin_status_ok'])
                || (int)($cachedData['server_listeners'] ?? 0) > 0
                || trim((string)($cachedData['title'] ?? '')) !== ''
            );
        $cacheLooksDead = $cacheHasUsefulData
            && (int)($cachedData['server_listeners'] ?? 0) <= 0
            && (int)($cachedData['current_listeners'] ?? 0) <= 0
            && trim((string)($cachedData['title'] ?? '')) !== '';
        if ($cacheHasUsefulData && !$cacheLooksDead) {
            return $cached = $cachedData;
        }
    }

    $show = ugfx_current_show();
    $fallback = [
        'ok' => false,
        'server_listeners' => 0,
        'current_listeners' => 0,
        'total_listener_connections' => 0,
        'title' => 'Underground FX Live',
        'artist' => $show['current'] ?? 'Underground FX Radio',
        'mount' => (string)($cfg['mount_auto'] ?? '/autodj'),
        'is_live' => false,
        'status' => 'LIVE',
        'stream_url' => $playerUrls['preferred'],
        'public_stream_url' => $playerUrls['public'],
        'direct_stream_url' => $playerUrls['direct'],
        'proxy_stream_url' => $playerUrls['proxy'],
    ];


    $publicXsl = ugfx_fetch_public_status_xsl();
    $parsedPublic = $publicXsl !== '' ? ugfx_parse_public_status_xsl($publicXsl) : ['global' => [], 'sources' => []];

    $needAdmin = true;

    $xml = '';
    $xsl = ugfx_fetch_icecast_xsl();
    if ($xsl === '') {
        $xml = ugfx_fetch_icecast_xml();
    }

    if ($xml === '' && $xsl === '' && $publicXsl === '') {
        if (is_file($cacheFile)) {
            $stale = json_decode((string)@file_get_contents($cacheFile), true);
            $staleIsUseful = is_array($stale)
                && (
                    (int)($stale['source_count'] ?? 0) > 0
                    || !empty($stale['public_status_ok'])
                    || !empty($stale['admin_status_ok'])
                    || (int)($stale['server_listeners'] ?? 0) > 0
                    || trim((string)($stale['title'] ?? '')) !== ''
                );
            if ($staleIsUseful) {
                $stale['ok'] = false;
                $stale['_warning'] = 'Icecast fetch failed (served stale cache)';
                return $cached = $stale;
            }
        }
        return $cached = $fallback;
    }

    $parsedXml = $xml !== '' ? ugfx_parse_icecast_data($xml) : ['global' => [], 'sources' => []];
    $parsedXsl = $xsl !== '' ? ugfx_parse_stats_xsl($xsl) : ['global' => [], 'sources' => []];

    $global = ugfx_merge_global_stats($parsedPublic['global'] ?? [], $parsedXsl['global'] ?? [], $parsedXml['global'] ?? []);
    $sources = ugfx_merge_icecast_sources(ugfx_merge_icecast_sources($parsedPublic['sources'] ?? [], $parsedXsl['sources'] ?? []), $parsedXml['sources'] ?? []);
    $active = ugfx_pick_preferred_source($sources) ?? [];

    $autoSource = null;
    foreach ($sources as $source) {
        if (($source['mount'] ?? '') === (string)$cfg['mount_auto']) {
            $autoSource = $source;
            break;
        }
    }

    $isLive = (($active['mount'] ?? '') === (string)$cfg['mount_main']) && (int)($active['listeners'] ?? 0) > 0;
    $rawTitle = trim((string)($active['x_icy_title'] ?? $active['title'] ?? ''));
    if ($rawTitle === '') {
        $rawTitle = trim((string)($active['title'] ?? $active['x_icy_title'] ?? ''));
    }
    $artist = trim((string)($active['server_name'] ?? ''));

    if ($isLive) {
        $title = $rawTitle !== '' ? $rawTitle : 'Live DJ On Air';
        if ($artist === '') $artist = $show['current'] ?? 'Underground FX Live';
    } else {
        $fallbackTitle = trim((string)($autoSource['x_icy_title'] ?? $autoSource['title'] ?? ''));
        if ($fallbackTitle === '') {
            $fallbackTitle = trim((string)($autoSource['title'] ?? $autoSource['x_icy_title'] ?? ''));
        }
        $title = $rawTitle !== '' ? $rawTitle : ($fallbackTitle !== '' ? $fallbackTitle : 'Underground FX Live');
        if ($artist === '') $artist = trim((string)($autoSource['server_name'] ?? 'FX Vibe Radio Flow'));
        if ($artist === '') $artist = 'FX Vibe Radio Flow';
    }

    $currentListeners = (int)($active['listeners'] ?? 0);
    $maxSourceListeners = 0;
    foreach ($sources as $sourceProbe) {
        $maxSourceListeners = max($maxSourceListeners, (int)($sourceProbe['listeners'] ?? 0));
    }
    $serverListeners = max(
        $currentListeners,
        (int)($autoSource['listeners'] ?? 0),
        $maxSourceListeners,
        (int)($global['listeners'] ?? 0)
    );
    if ($serverListeners <= 0 && $currentListeners > 0) {
        $serverListeners = $currentListeners;
    }
    $displayListeners = (int)($global['listener_connections'] ?? 0);
    if ($displayListeners <= 0) {
        $displayListeners = max(
            (int)($global['listeners'] ?? 0),
            $serverListeners,
            (int)($autoSource['listeners'] ?? 0),
            $currentListeners
        );
    }

    $data = [
        'ok' => true,
        'server_listeners' => $displayListeners,
        'current_listeners' => $currentListeners,
        'display_listeners' => $displayListeners,
        'total_listener_connections' => (int)($global['listener_connections'] ?? 0),
        'global_listeners' => (int)($global['listeners'] ?? 0),
        'public_status_ok' => ($publicXsl !== ''),
        'admin_status_ok' => ($xml !== '' || $xsl !== ''),
        'title' => $title,
        'artist' => $artist,
        'mount' => (string)($active['mount'] ?? ($cfg['mount_auto'] ?? '/autodj')),
        'display_show' => ($isLive ? 'Live DJ On Air' : 'FX Vibe Radio Flow'),
        'is_live' => $isLive,
        'status' => 'LIVE',
        'stream_url' => $playerUrls['preferred'],
        'public_stream_url' => $playerUrls['public'],
        'direct_stream_url' => $playerUrls['direct'],
        'proxy_stream_url' => $playerUrls['proxy'],
        'sources' => $sources,
        'source_count' => count($sources),
        'ts' => time(),
    ];

    $shouldWriteCache = (
        (int)($data['source_count'] ?? 0) > 0
        || !empty($data['public_status_ok'])
        || !empty($data['admin_status_ok'])
        || (int)($data['server_listeners'] ?? 0) > 0
        || trim((string)($data['title'] ?? '')) !== ''
    );
    if ($shouldWriteCache) {
        @file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), LOCK_EX);
    } elseif (is_file($cacheFile)) {
        @unlink($cacheFile);
    }
    return $cached = $data;
}



function ugfx_listener_count_fast(): int {
    static $count = null;
    if ($count !== null) return $count;

    $public = ugfx_parse_public_status_xsl(ugfx_fetch_public_status_xsl());
    $publicConnections = (int)($public['global']['listener_connections'] ?? 0);

    $adminXsl = ugfx_fetch_icecast_xsl();
    $admin = $adminXsl !== '' ? ugfx_parse_stats_xsl($adminXsl) : ugfx_parse_icecast_data(ugfx_fetch_icecast_xml());
    $adminConnections = (int)($admin['global']['listener_connections'] ?? 0);

    $status = ugfx_icecast_status();
    $statusConnections = (int)($status['total_listener_connections'] ?? 0);

    $bestConnections = max($publicConnections, $adminConnections, $statusConnections);
    if ($bestConnections > 0) {
        return $count = $bestConnections;
    }

    $publicListeners = (int)($public['global']['listeners'] ?? 0);
    $adminListeners = (int)($admin['global']['listeners'] ?? 0);
    $statusListeners = (int)($status['server_listeners'] ?? 0);

    return $count = max($publicListeners, $adminListeners, $statusListeners, 0);
}
function ugfx_now_playing(): array {
    $show = ugfx_current_show();
    $icecast = ugfx_icecast_status();
    return [
        'title' => $icecast['title'] ?: 'Underground FX Live',
        'artist' => $icecast['artist'] ?: $show['current'],
        'display_show' => $icecast['display_show'] ?? ($show['current'] ?? 'FX Vibe Radio Flow'),
        'album' => 'Underground FX Radio',
        'art' => ugfx_asset('img/brand/ugfx-hero.png'),
        'listeners' => ugfx_listener_count_fast(),
        'current_listeners' => (int)($icecast['current_listeners'] ?? 0),
        'total_listener_connections' => (int)($icecast['total_listener_connections'] ?? 0),
        'status' => $icecast['status'] ?? 'LIVE',
        'mount' => $icecast['mount'] ?? '',
        'is_live' => !empty($icecast['is_live']),
        'stream_url' => $icecast['stream_url'] ?? '',
        'public_stream_url' => $icecast['public_stream_url'] ?? '',
        'direct_stream_url' => $icecast['direct_stream_url'] ?? '',
        'proxy_stream_url' => $icecast['proxy_stream_url'] ?? '',
    ];
}


function ugfx_compute_listener_display_count(): int {
    return ugfx_listener_count_fast();
}

function ugfx_nav_items(): array {
    return [
        ['label' => 'Home', 'href' => 'index.php'],
        ['label' => 'Listen Live', 'href' => 'listen-live.php'],
        ['label' => 'Schedule', 'href' => 'schedule.php'],
        ['label' => 'Requests', 'href' => 'requests.php'],
        ['label' => 'News', 'href' => 'news.php'],
        ['label' => 'Team', 'href' => 'team.php'],
        ['label' => 'Join', 'href' => 'join.php'],
        ['label' => 'Events', 'href' => 'events.php'],
        ['label' => 'Friends', 'href' => 'friends.php'],
        ['label' => 'Chat', 'href' => 'chat.php'],
        ['label' => 'Contact', 'href' => 'contact.php'],
    ];
}

function ugfx_safe_links(string $text): string {
    $escaped = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8'));
    return preg_replace_callback('/(https?:\/\/[^\s<]+)/i', function ($matches) {
        $url = htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8');
        return '<a href="' . $url . '" target="_blank" rel="noopener noreferrer nofollow">' . $url . '</a>';
    }, $escaped);
}


function ugfx_contains_banned_word(string $text): bool {
    global $ugfx;
    $words = ugfx_read_json($ugfx['storage']['banned_words'], []);
    if (!is_array($words) || !$words) return false;
    $lower = mb_strtolower($text, 'UTF-8');
    foreach ($words as $word) {
        $word = trim((string)$word);
        if ($word === '') continue;
        if (mb_stripos($lower, mb_strtolower($word, 'UTF-8'), 0, 'UTF-8') !== false) {
            return true;
        }
    }
    return false;
}

function ugfx_chat_messages(int $limit = 150): array {
    global $ugfx;
    $items = ugfx_read_json($ugfx['storage']['chat'], []);
    if (!is_array($items)) return [];
    $items = array_values(array_filter($items, function ($item) {
        return is_array($item) && !empty($item['id']);
    }));
    usort($items, function ($a, $b) {
        return strtotime((string)($a['created_at'] ?? '')) <=> strtotime((string)($b['created_at'] ?? ''));
    });
    if ($limit > 0 && count($items) > $limit) {
        $items = array_slice($items, -$limit);
    }
    return $items;
}

function ugfx_chat_add(array $message): array {
    global $ugfx;
    $items = ugfx_chat_messages(0);
    $id = (string)(time()) . bin2hex(random_bytes(4));
    $entry = [
        'id' => $message['id'] ?? $id,
        'name' => trim((string)($message['name'] ?? 'Guest')) ?: 'Guest',
        'message' => trim((string)($message['message'] ?? '')),
        'gif_url' => trim((string)($message['gif_url'] ?? '')),
        'sticker' => trim((string)($message['sticker'] ?? '')),
        'voice_url' => trim((string)($message['voice_url'] ?? '')),
        'voice_mime' => trim((string)($message['voice_mime'] ?? 'audio/webm')),
        'text_color' => preg_match('/^#[0-9A-Fa-f]{6}$/', (string)($message['text_color'] ?? '')) ? (string)$message['text_color'] : '#f3f7ff',
        'avatar' => trim((string)($message['avatar'] ?? '')),
        'type' => trim((string)($message['type'] ?? 'user')) ?: 'user',
        'created_at' => trim((string)($message['created_at'] ?? date('Y-m-d H:i:s'))),
    ];
    $items[] = $entry;
    ugfx_cleanup_chat_uploads(30);
    if (count($items) > 250) {
        $items = array_slice($items, -250);
    }
    ugfx_write_json($ugfx['storage']['chat'], array_values($items));
    return $entry;
}


function ugfx_base_url(): string {
    global $ugfx;
    $configured = trim((string)($ugfx['site_url'] ?? ''));
    if ($configured !== '' && preg_match('#^https?://#i', $configured)) {
        return rtrim($configured, '/');
    }
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
    $scheme = $https ? 'https' : 'http';
    $host = trim((string)($_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost')) ?: 'localhost';
    return $scheme . '://' . $host;
}

function ugfx_absolute_url(string $path = ''): string {
    $base = rtrim(ugfx_base_url(), '/');
    if ($path === '' || $path === '/') {
        return $base . '/';
    }
    if (preg_match('#^https?://#i', $path)) {
        return $path;
    }
    return $base . '/' . ltrim($path, '/');
}

function ugfx_current_url(): string {
    $uri = (string)($_SERVER['REQUEST_URI'] ?? '/');
    return ugfx_absolute_url($uri);
}

function ugfx_page_seo(string $currentPage, string $pageTitle = ''): array {
    global $ugfx;
    $siteName = (string)($ugfx['site_name'] ?? 'Underground FX Radio');
    $tagline = trim((string)($ugfx['tagline'] ?? 'Underground FX Radio live.'));
    $path = strtolower(pathinfo($currentPage, PATHINFO_FILENAME));
    $absolutePath = dirname(__DIR__);
    $descriptions = [
        'index' => 'Listen live to Underground FX Radio for nonstop music, live DJs, station news, requests, chat, events, and the full weekly schedule.',
        'listen-live' => 'Open the Underground FX live radio player and stream the station online with now playing information and direct access to the broadcast.',
        'schedule' => 'See the Underground FX Radio weekly schedule, live right now show, up next listing, and presenter timetable.',
        'requests' => 'Send a song request or shout-out to Underground FX Radio and connect directly with the station request line.',
        'news' => 'Read the latest Underground FX Radio news, station updates, announcements, featured shows, and community updates.',
        'team' => 'Meet the Underground FX presenters, DJs, and station team behind the shows and community programming.',
        'join' => 'Apply to join Underground FX Radio as a DJ, presenter, volunteer, or support team member.',
        'events' => 'Discover Underground FX and Broadcasturself events, featured promotions, music hunts, and station specials.',
        'friends' => 'Explore Underground FX friends, supporters, partner stations, and network links.',
        'chat' => 'Join the Underground FX website chat and connect with listeners, presenters, and the station community.',
        'contact' => 'Contact Underground FX Radio for station enquiries, promotions, partnerships, feedback, and support.',
        'charts' => 'Browse Underground FX charts, featured tracks, and station favourites.',
        'page' => $tagline,
        'status' => 'Underground FX Radio service status and station information.',
        'player-popup' => 'Underground FX Radio popout player.',
    ];
    $robots = [
        'chat' => 'noindex,follow,max-image-preview:large',
        'status' => 'noindex,nofollow',
        'player-popup' => 'noindex,nofollow',
    ];
    $description = $descriptions[$path] ?? $tagline;
    if ($path === 'page' && isset($_GET['slug'])) {
        $description = 'Read more from Underground FX Radio.';
    }
    $imagePath = 'assets/img/brand/ugfx-og-cover.jpg';
    if (!is_file($absolutePath . '/' . $imagePath)) {
        $imagePath = 'assets/img/brand/ugfx-hero.png';
    }
    $image = ugfx_absolute_url($imagePath);
    $imageType = str_ends_with(strtolower($imagePath), '.png') ? 'image/png' : 'image/jpeg';
    $imageWidth = 1200;
    $imageHeight = 630;
    if (function_exists('getimagesize')) {
        $imageSize = @getimagesize($absolutePath . '/' . $imagePath);
        if (is_array($imageSize)) {
            $imageWidth = (int)($imageSize[0] ?? $imageWidth);
            $imageHeight = (int)($imageSize[1] ?? $imageHeight);
            if (!empty($imageSize['mime'])) {
                $imageType = (string)$imageSize['mime'];
            }
        }
    }
    $canonical = ugfx_current_url();
    $type = $path === 'index' ? 'website' : 'article';
    $title = trim($pageTitle) ?: $siteName;
    return [
        'title' => $title,
        'description' => $description,
        'robots' => $robots[$path] ?? 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1',
        'canonical' => $canonical,
        'image' => $image,
        'image_secure' => preg_replace('#^http://#i', 'https://', $image),
        'image_alt' => $siteName . ' social preview artwork',
        'image_type' => $imageType,
        'image_width' => $imageWidth,
        'image_height' => $imageHeight,
        'site_name' => $siteName,
        'type' => $type,
        'locale' => 'en_AU',
        'theme_color' => '#111418',
        'twitter_site' => (string)($ugfx['twitter_handle'] ?? ''),
        'section' => ucfirst($path === 'index' ? 'Home' : str_replace('-', ' ', $path)),
    ];
}

function ugfx_seo_json_ld(array $seo = []): string {
    global $ugfx;
    $siteName = (string)($ugfx['site_name'] ?? 'Underground FX Radio');
    $tagline = trim((string)($ugfx['tagline'] ?? 'Underground FX Radio live.'));
    $url = $seo['canonical'] ?? ugfx_current_url();
    $logo = $seo['image'] ?? ugfx_absolute_url('assets/img/brand/ugfx-og-cover.jpg');
    $sameAs = [];
    if (!empty($ugfx['socials']) && is_array($ugfx['socials'])) {
        foreach ($ugfx['socials'] as $social) {
            if (is_string($social) && preg_match('#^https?://#i', $social)) {
                $sameAs[] = $social;
            } elseif (is_array($social) && !empty($social['url']) && preg_match('#^https?://#i', (string)$social['url'])) {
                $sameAs[] = (string)$social['url'];
            }
        }
    }
    $graph = [
        [
            '@context' => 'https://schema.org',
            '@type' => 'Organization',
            '@id' => ugfx_base_url() . '#organization',
            'name' => $siteName,
            'url' => rtrim(ugfx_base_url(), '/') . '/',
            'logo' => [
                '@type' => 'ImageObject',
                'url' => $logo,
            ],
            'description' => $tagline,
            'sameAs' => array_values(array_unique($sameAs)),
        ],
        [
            '@context' => 'https://schema.org',
            '@type' => 'WebSite',
            '@id' => ugfx_base_url() . '#website',
            'url' => rtrim(ugfx_base_url(), '/') . '/',
            'name' => $siteName,
            'publisher' => ['@id' => ugfx_base_url() . '#organization'],
            'inLanguage' => 'en-AU',
        ],
        [
            '@context' => 'https://schema.org',
            '@type' => 'WebPage',
            '@id' => $url . '#webpage',
            'url' => $url,
            'name' => (string)($seo['title'] ?? $siteName),
            'description' => (string)($seo['description'] ?? $tagline),
            'isPartOf' => ['@id' => ugfx_base_url() . '#website'],
            'primaryImageOfPage' => [
                '@type' => 'ImageObject',
                'url' => $logo,
            ],
            'inLanguage' => 'en-AU',
        ],
    ];
    return json_encode($graph, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
