<?php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: public, max-age=120');
header('X-Content-Type-Options: nosniff');

if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') {
    http_response_code(405);
    echo json_encode(['ok' => false, 'error' => 'method'], JSON_UNESCAPED_UNICODE);
    exit;
}

const MOCKER_YT_CACHE_TTL = 600;
const MOCKER_YT_MAX_VIDEOS = 8;

require_once __DIR__ . '/config.php';

function mocker_yt_json_out(array $payload, int $code = 200): void
{
    http_response_code($code);
    echo json_encode($payload, JSON_UNESCAPED_UNICODE);
    exit;
}

function mocker_yt_read_json(string $path): ?array
{
    if (!is_readable($path)) {
        return null;
    }
    $raw = file_get_contents($path);
    if ($raw === false || $raw === '') {
        return null;
    }
    $data = json_decode($raw, true);

    return is_array($data) ? $data : null;
}

function mocker_yt_http_get(string $url): ?string
{
    $headers = [
        'Accept: application/xml,text/html,*/*',
        'Accept-Language: tr-TR,tr;q=0.9,en;q=0.8',
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
    ];

    if (function_exists('curl_init')) {
        foreach ([true, false] as $verifySsl) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_TIMEOUT => 18,
                CURLOPT_SSL_VERIFYPEER => $verifySsl,
                CURLOPT_SSL_VERIFYHOST => $verifySsl ? 2 : 0,
                CURLOPT_HTTPHEADER => $headers,
            ]);
            $body = curl_exec($ch);
            $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            if ($body !== false && $code >= 200 && $code < 400) {
                return (string) $body;
            }
        }

        return null;
    }

    foreach ([true, false] as $verifySsl) {
        $ctx = stream_context_create([
            'http' => [
                'timeout' => 18,
                'header' => implode("\r\n", $headers) . "\r\n",
            ],
            'ssl' => [
                'verify_peer' => $verifySsl,
                'verify_peer_name' => $verifySsl,
            ],
        ]);
        $body = @file_get_contents($url, false, $ctx);
        if ($body !== false) {
            return (string) $body;
        }
    }

    return null;
}

function mocker_yt_persist_feed(array $feed): void
{
    if (empty($feed['ok'])) {
        return;
    }
    $json = json_encode($feed, JSON_UNESCAPED_UNICODE);
    if ($json === false) {
        return;
    }
    $root = dirname(__DIR__);
    $cacheDir = $root . '/data/cache';
    if (!is_dir($cacheDir)) {
        @mkdir($cacheDir, 0755, true);
    }
    @file_put_contents($cacheDir . '/youtube_feed.json', $json, LOCK_EX);
    @file_put_contents($root . '/data/youtube_feed.json', $json, LOCK_EX);
}

function mocker_yt_stale_feed(string $channelId): ?array
{
    $root = dirname(__DIR__);
    foreach ([$root . '/data/cache/youtube_feed.json', $root . '/data/youtube_feed.json'] as $path) {
        $data = mocker_yt_read_json($path);
        if (
            is_array($data)
            && !empty($data['ok'])
            && is_array($data['videos'] ?? null)
            && count($data['videos']) > 0
            && ($channelId === '' || ($data['channelId'] ?? '') === $channelId)
        ) {
            $data['stale'] = true;

            return $data;
        }
    }

    return null;
}

function mocker_yt_config(): array
{
    $root = dirname(__DIR__);
    $site = mocker_yt_read_json($root . '/data/site.json') ?? [];
    $yt = is_array($site['youtube'] ?? null) ? $site['youtube'] : [];

    $channelId = trim((string) ($yt['channelId'] ?? ''));
    $handle = trim((string) ($yt['handle'] ?? ''));
    $channelUrl = trim((string) ($yt['channelUrl'] ?? ''));

    if ($channelId === '' || ($handle === '' && $channelUrl === '')) {
        $social = mocker_yt_read_json($root . '/data/social.json');
        if (is_array($social)) {
            foreach ($social as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $href = trim((string) ($row['href'] ?? ''));
                if ($href === '' || stripos($href, 'youtube') === false) {
                    continue;
                }
                if ($channelUrl === '') {
                    $channelUrl = strtok($href, '?') ?: $href;
                }
                if ($handle === '' && preg_match('#youtube\.com/@([^/?#]+)#i', $href, $m)) {
                    $handle = trim((string) $m[1]);
                }
                break;
            }
        }
    }

    if ($channelUrl === '' && $handle !== '') {
        $channelUrl = 'https://www.youtube.com/@' . rawurlencode($handle);
    }
    if ($channelUrl === '' && $channelId !== '') {
        $channelUrl = 'https://www.youtube.com/channel/' . rawurlencode($channelId);
    }

    $maxVideos = (int) ($yt['maxVideos'] ?? MOCKER_YT_MAX_VIDEOS);
    if ($maxVideos < 1) {
        $maxVideos = 4;
    }
    if ($maxVideos > 12) {
        $maxVideos = 12;
    }

    return [
        'channelId' => $channelId,
        'handle' => $handle,
        'channelUrl' => $channelUrl,
        'maxVideos' => $maxVideos,
    ];
}

function mocker_yt_resolve_channel_id(string $handle, string $knownId): string
{
    if ($knownId !== '') {
        return $knownId;
    }
    if ($handle === '') {
        return '';
    }

    $cacheDir = dirname(__DIR__) . '/data/cache';
    $cacheFile = $cacheDir . '/youtube_channel.json';
    $cached = mocker_yt_read_json($cacheFile);
    if (
        is_array($cached)
        && ($cached['handle'] ?? '') === $handle
        && !empty($cached['channelId'])
        && (int) ($cached['fetchedAt'] ?? 0) > time() - 86400 * 14
    ) {
        return (string) $cached['channelId'];
    }

    $html = mocker_yt_http_get('https://www.youtube.com/@' . rawurlencode($handle));
    if ($html === null) {
        return '';
    }

    $channelId = '';
    if (preg_match('/"channelId":"(UC[a-zA-Z0-9_-]{22})"/', $html, $m)) {
        $channelId = $m[1];
    } elseif (preg_match('/"browseId":"(UC[a-zA-Z0-9_-]{22})"/', $html, $m)) {
        $channelId = $m[1];
    } elseif (preg_match('#/channel/(UC[a-zA-Z0-9_-]{22})#', $html, $m)) {
        $channelId = $m[1];
    }

    if ($channelId !== '') {
        if (!is_dir($cacheDir)) {
            @mkdir($cacheDir, 0755, true);
        }
        @file_put_contents(
            $cacheFile,
            json_encode(
                ['handle' => $handle, 'channelId' => $channelId, 'fetchedAt' => time()],
                JSON_UNESCAPED_UNICODE
            ),
            LOCK_EX
        );
    }

    return $channelId;
}

function mocker_yt_channel_tab_url(string $handle, string $channelId, string $tab): string
{
    $tab = trim($tab, '/');
    if ($handle !== '') {
        return 'https://www.youtube.com/@' . rawurlencode($handle) . '/' . $tab;
    }

    return 'https://www.youtube.com/channel/' . rawurlencode($channelId) . '/' . $tab;
}

function mocker_yt_context_is_short(string $context): bool
{
    if ($context === '') {
        return false;
    }

    return (bool) preg_match(
        '#"url":"/shorts/|reelWatchEndpoint|reelRenderer|"isShort":true|"SHORTS"|shortsShelf|shortsAvatar|#Shorts#i',
        $context
    );
}

function mocker_yt_title_looks_like_short(string $title): bool
{
    $t = strtolower(trim($title));
    if ($t === '') {
        return false;
    }
    if (str_contains($t, '#shorts') || str_contains($t, '#short')) {
        return true;
    }
    if (str_contains($t, '#slotoyunları') || str_contains($t, '#slotoyunlari')) {
        return true;
    }
    if (preg_match('/#casino\s*$/u', $t)) {
        return true;
    }

    return false;
}

/**
 * @param list<array<string, mixed>> $videos
 *
 * @return list<array<string, mixed>>
 */
function mocker_yt_filter_stream_feed_videos(array $videos, string $source = ''): array
{
    $trustTab = in_array($source, ['streams', 'api_streams'], true);
    $out = [];
    foreach ($videos as $v) {
        if (!is_array($v)) {
            continue;
        }
        $title = (string) ($v['title'] ?? '');
        if (mocker_yt_title_looks_like_short($title)) {
            continue;
        }
        if ($trustTab || !empty($v['isStream']) || !empty($v['isLive'])) {
            $v['isStream'] = true;
            $out[] = $v;
            continue;
        }
        if (preg_match('/\b(canli|canlı|yayın|yayin|live|stream)\b/ui', $title)) {
            $v['isStream'] = true;
            $out[] = $v;
        }
    }

    return $out;
}

/**
 * @return list<array<string, mixed>>
 */
function mocker_yt_extract_ids_from_html(string $html, int $limit): array
{
    if ($html === '' || $limit < 1) {
        return [];
    }
    if (!preg_match_all('/"videoId":"([a-zA-Z0-9_-]{11})"/', $html, $m)) {
        return [];
    }
    $videos = [];
    $seen = [];
    foreach ($m[1] as $videoId) {
        if (count($videos) >= $limit) {
            break;
        }
        if (isset($seen[$videoId])) {
            continue;
        }
        $seen[$videoId] = true;
        $videos[] = [
            'id' => $videoId,
            'title' => '',
            'url' => 'https://www.youtube.com/watch?v=' . rawurlencode($videoId),
            'thumbnail' => 'https://i.ytimg.com/vi/' . rawurlencode($videoId) . '/hqdefault.jpg',
            'publishedAt' => '',
            'isLive' => (bool) preg_match(
                '#"style":"LIVE"|"isLiveNow":true#',
                $html
            ),
            'isStream' => true,
        ];
    }

    return $videos;
}

/**
 * YouTube Data API: aktif ve geçmiş canlı yayınlar.
 *
 * @return list<array<string, mixed>>
 */
function mocker_yt_fetch_streams_via_api(string $channelId, int $limit): array
{
    if (!defined('YOUTUBE_API_KEY') || YOUTUBE_API_KEY === '' || $channelId === '') {
        return [];
    }

    $videos = [];
    $seen = [];
    foreach (['live', 'completed'] as $eventType) {
        $url = 'https://www.googleapis.com/youtube/v3/search?part=snippet&channelId='
            . rawurlencode($channelId)
            . '&type=video&eventType='
            . rawurlencode($eventType)
            . '&order=date&maxResults='
            . min(25, max(1, $limit))
            . '&key='
            . rawurlencode(YOUTUBE_API_KEY);
        $raw = mocker_yt_http_get($url);
        if ($raw === null) {
            continue;
        }
        $j = json_decode($raw, true);
        if (!is_array($j) || !is_array($j['items'] ?? null)) {
            continue;
        }
        foreach ($j['items'] as $item) {
            if (count($videos) >= $limit) {
                break 2;
            }
            if (!is_array($item)) {
                continue;
            }
            $id = (string) ($item['id']['videoId'] ?? '');
            if ($id === '' || isset($seen[$id])) {
                continue;
            }
            $seen[$id] = true;
            $sn = is_array($item['snippet'] ?? null) ? $item['snippet'] : [];
            $title = trim((string) ($sn['title'] ?? ''));
            if (mocker_yt_title_looks_like_short($title)) {
                continue;
            }
            $thumb = '';
            $thumbs = is_array($sn['thumbnails'] ?? null) ? $sn['thumbnails'] : [];
            foreach (['high', 'medium', 'default'] as $k) {
                if (!empty($thumbs[$k]['url'])) {
                    $thumb = (string) $thumbs[$k]['url'];
                    break;
                }
            }
            if ($thumb === '') {
                $thumb = 'https://i.ytimg.com/vi/' . rawurlencode($id) . '/hqdefault.jpg';
            }
            $videos[] = [
                'id' => $id,
                'title' => $title,
                'url' => 'https://www.youtube.com/watch?v=' . rawurlencode($id),
                'thumbnail' => $thumb,
                'publishedAt' => (string) ($sn['publishedAt'] ?? ''),
                'isLive' => $eventType === 'live',
                'isStream' => true,
            ];
        }
    }

    return array_slice($videos, 0, $limit);
}

function mocker_yt_context_is_stream(string $context): bool
{
    if ($context === '') {
        return false;
    }

    return (bool) preg_match(
        '#"style":"BROADCAST"|"style":"LIVE"|"isLiveNow":true|"pastLiveStream"|"scheduledLive"|streamed|was live|Yayınlandı|yayınlandı|CANLI|YAYINDA#i',
        $context
    );
}

function mocker_yt_detect_live_id(string $channelId, string $handle): ?string
{
    if (defined('YOUTUBE_API_KEY') && YOUTUBE_API_KEY !== '') {
        $url = 'https://www.googleapis.com/youtube/v3/search?part=id&eventType=live&type=video&maxResults=1&channelId='
            . rawurlencode($channelId)
            . '&key='
            . rawurlencode(YOUTUBE_API_KEY);
        $raw = mocker_yt_http_get($url);
        if ($raw !== null) {
            $j = json_decode($raw, true);
            $vid = $j['items'][0]['id']['videoId'] ?? null;
            if (is_string($vid) && preg_match('/^[a-zA-Z0-9_-]{11}$/', $vid)) {
                return $vid;
            }
        }
    }

    foreach (['live', 'streams'] as $tab) {
        $html = mocker_yt_http_get(mocker_yt_channel_tab_url($handle, $channelId, $tab));
        if ($html === null) {
            continue;
        }
        if (!preg_match('/"isLive(?:Now|Content|Broadcast)":true|"style":"LIVE"|"badgeText":"LIVE"|"text":"\s*watching\s*"/i', $html)) {
            continue;
        }
        if (preg_match('/"videoId":"([a-zA-Z0-9_-]{11})"/', $html, $m)) {
            return $m[1];
        }
        if (preg_match('#/watch\?v=([a-zA-Z0-9_-]{11})#', $html, $m)) {
            return $m[1];
        }
    }

    return null;
}

/**
 * Handle sayfasının HTML'inden son videoları çıkarır.
 * RSS feed yeni/küçük kanallar için 404 dönebilir; bu fallback aynı veriyi
 * kanal sayfasının yapısından (yeni "lockupViewModel" layout dahil) toplar.
 *
 * @return list<array<string, mixed>>
 */
/**
 * @param 'streams'|'videos' $mode streams = yalnızca canlı yayın geçmişi; videos = genel (shorts filtrelenir)
 *
 * @return list<array<string, mixed>>
 */
function mocker_yt_extract_videos_from_html(string $html, int $limit, string $mode = 'streams'): array
{
    if ($html === '' || $limit < 1) {
        return [];
    }

    $videos = [];
    $seen = [];
    $scanLimit = min($limit * 4, 48);

    if (preg_match_all(
        '#"videoRenderer":\{"videoId":"([a-zA-Z0-9_-]{11})"(.{0,5000}?)"title":\{(?:"runs":\[\{"text":"|"simpleText":")([^"\\\\]+)"#s',
        $html,
        $matches,
        PREG_SET_ORDER
    )) {
        foreach ($matches as $m) {
            if (count($videos) >= $limit) {
                break;
            }
            $videoId = $m[1];
            if (isset($seen[$videoId])) {
                continue;
            }

            $context = $m[2] ?? '';
            if (mocker_yt_context_is_short($context)) {
                continue;
            }

            $seen[$videoId] = true;

            $rawTitle = $m[3] ?? '';
            $title = '';
            if ($rawTitle !== '') {
                $decoded = json_decode('"' . $rawTitle . '"');
                $title = is_string($decoded) ? trim($decoded) : trim($rawTitle);
            }

            $publishedAt = '';
            if (preg_match('#"publishedTimeText":\{"simpleText":"([^"\\\\]+)"#', $context, $pm)) {
                $publishedAt = trim($pm[1]);
            }

            $isLive = (bool) preg_match(
                '#"style":"LIVE"|"isLiveNow":true|"label":"\s*(?:LIVE|CANLI|YAYINDA)\s*"#i',
                $context
            );

            $videos[] = [
                'id' => $videoId,
                'title' => $title,
                'url' => 'https://www.youtube.com/watch?v=' . rawurlencode($videoId),
                'thumbnail' => 'https://i.ytimg.com/vi/' . rawurlencode($videoId) . '/hqdefault.jpg',
                'publishedAt' => $publishedAt,
                'isLive' => $isLive,
                'isStream' => $mode === 'streams' || mocker_yt_context_is_stream($context) || $isLive,
            ];
        }
    }

    if (count($videos) < $scanLimit && preg_match_all(
        '#"gridVideoRenderer":\{"videoId":"([a-zA-Z0-9_-]{11})"(.{0,5000}?)"title":\{(?:"runs":\[\{"text":"|"simpleText":")([^"\\\\]+)"#s',
        $html,
        $gridMatches,
        PREG_SET_ORDER
    )) {
        foreach ($gridMatches as $m) {
            if (count($videos) >= $limit) {
                break;
            }
            $videoId = $m[1];
            if (isset($seen[$videoId])) {
                continue;
            }
            $context = $m[2] ?? '';
            if (mocker_yt_context_is_short($context)) {
                continue;
            }
            $seen[$videoId] = true;
            $rawTitle = $m[3] ?? '';
            $title = '';
            if ($rawTitle !== '') {
                $decoded = json_decode('"' . $rawTitle . '"');
                $title = is_string($decoded) ? trim($decoded) : trim($rawTitle);
            }
            $videos[] = [
                'id' => $videoId,
                'title' => $title,
                'url' => 'https://www.youtube.com/watch?v=' . rawurlencode($videoId),
                'thumbnail' => 'https://i.ytimg.com/vi/' . rawurlencode($videoId) . '/hqdefault.jpg',
                'publishedAt' => '',
                'isLive' => mocker_yt_context_is_stream($context) && (bool) preg_match('#"style":"LIVE"|"isLiveNow":true#i', $context),
                'isStream' => true,
            ];
        }
    }

    if ($mode === 'videos' && count($videos) < $limit && preg_match_all(
        '#"watchEndpoint":\{"videoId":"([a-zA-Z0-9_-]{11})"#',
        $html,
        $idMatches
    )) {
        foreach ($idMatches[1] as $videoId) {
            if (count($videos) >= $limit) {
                break;
            }
            if (isset($seen[$videoId])) {
                continue;
            }
            $seen[$videoId] = true;
            $videos[] = [
                'id' => $videoId,
                'title' => '',
                'url' => 'https://www.youtube.com/watch?v=' . rawurlencode($videoId),
                'thumbnail' => 'https://i.ytimg.com/vi/' . rawurlencode($videoId) . '/hqdefault.jpg',
                'publishedAt' => '',
                'isLive' => false,
                'isStream' => false,
            ];
        }
    }

    return array_slice($videos, 0, $limit);
}

/**
 * Bir video başlığını çözmek için önce oEmbed (hızlı, küçük), başarısızsa
 * /watch sayfasının <title> etiketini dener. Sonuç 30 gün dosyada cache'lenir;
 * embed-disabled / kısıtlı / canlı yayın geçmişlerinde oEmbed 401 dönebildiği
 * için iki yollu bir geri çekilme zinciri kullanılıyor.
 */
function mocker_yt_resolve_title(string $videoId): string
{
    static $cache = [];
    if (isset($cache[$videoId])) {
        return $cache[$videoId];
    }

    $cacheDir = dirname(__DIR__) . '/data/cache/youtube_titles';
    if (!is_dir($cacheDir)) {
        @mkdir($cacheDir, 0755, true);
    }
    $cachePath = $cacheDir . '/' . $videoId . '.json';
    if (is_readable($cachePath)) {
        $cached = json_decode((string) file_get_contents($cachePath), true);
        if (is_array($cached) && isset($cached['title']) && (int) ($cached['fetchedAt'] ?? 0) > time() - 86400 * 30) {
            $cache[$videoId] = (string) $cached['title'];

            return $cache[$videoId];
        }
    }

    $title = '';

    $oembedUrl = 'https://www.youtube.com/oembed?format=json&url='
        . rawurlencode('https://www.youtube.com/watch?v=' . $videoId);
    $body = mocker_yt_http_get($oembedUrl);
    if ($body !== null && $body !== '' && stripos(ltrim($body), '{') === 0) {
        $decoded = json_decode($body, true);
        if (is_array($decoded) && isset($decoded['title']) && is_string($decoded['title'])) {
            $title = trim($decoded['title']);
        }
    }

    if ($title === '') {
        $watchUrl = 'https://www.youtube.com/watch?v=' . rawurlencode($videoId);
        $watchHtml = mocker_yt_http_get($watchUrl);
        if ($watchHtml !== null && $watchHtml !== '') {
            if (preg_match('#<meta\s+name="title"\s+content="([^"]+)"#i', $watchHtml, $mm)) {
                $title = trim(html_entity_decode($mm[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
            }
            if ($title === '' && preg_match('#<title>(.*?)</title>#is', $watchHtml, $mm)) {
                $title = trim(html_entity_decode($mm[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
                $title = (string) preg_replace('/\s*-\s*YouTube\s*$/i', '', $title);
            }
        }
    }

    if ($title !== '') {
        @file_put_contents(
            $cachePath,
            json_encode(['title' => $title, 'fetchedAt' => time()], JSON_UNESCAPED_UNICODE)
        );
    }
    $cache[$videoId] = $title;

    return $title;
}

/**
 * Eksik başlıkları oEmbed üzerinden tamamlar. En fazla $limit oEmbed isteği yapar.
 *
 * @param list<array<string, mixed>> $videos
 *
 * @return list<array<string, mixed>>
 */
function mocker_yt_enrich_titles(array $videos, int $limit = 6): array
{
    $remaining = $limit;
    foreach ($videos as &$v) {
        if ($remaining <= 0) {
            break;
        }
        $title = (string) ($v['title'] ?? '');
        $videoId = (string) ($v['id'] ?? '');
        if ($title !== '' || $videoId === '') {
            continue;
        }
        $fetched = mocker_yt_resolve_title($videoId);
        if ($fetched !== '') {
            $v['title'] = $fetched;
        }
        $remaining--;
    }
    unset($v);

    return $videos;
}

/**
 * @return list<array<string, mixed>>
 */
function mocker_yt_rss_entry_is_short(\SimpleXMLElement $entry): bool
{
    $media = $entry->children('http://search.yahoo.com/mrss/');
    if (!isset($media->group)) {
        return false;
    }
    $group = $media->group;
    foreach (['thumbnail', 'content'] as $nodeName) {
        if (!isset($group->{$nodeName})) {
            continue;
        }
        foreach ($group->{$nodeName} as $node) {
            $w = (int) ($node['width'] ?? 0);
            $h = (int) ($node['height'] ?? 0);
            if ($w > 0 && $h > $w * 1.05) {
                return true;
            }
        }
    }

    $title = strtolower(trim((string) ($entry->title ?? '')));

    return str_contains($title, '#shorts') || str_contains($title, '#short');
}

/**
 * @return list<array<string, mixed>>
 */
function mocker_yt_parse_rss(string $xml, int $limit, bool $streamsOnly = true): array
{
    $sx = @simplexml_load_string($xml);
    if ($sx === false) {
        return [];
    }
    $sx->registerXPathNamespace('atom', 'http://www.w3.org/2005/Atom');
    $sx->registerXPathNamespace('yt', 'http://www.youtube.com/xml/schemas/2015');
    $sx->registerXPathNamespace('media', 'http://search.yahoo.com/mrss/');

    $videos = [];
    $scanLimit = min($limit * 6, 72);
    foreach ($sx->entry as $entry) {
        if (count($videos) >= $limit) {
            break;
        }
        if ($streamsOnly && mocker_yt_rss_entry_is_short($entry)) {
            continue;
        }
        $ns = $entry->children('http://www.youtube.com/xml/schemas/2015');
        $media = $entry->children('http://search.yahoo.com/mrss/');
        $videoId = trim((string) ($ns->videoId ?? ''));
        if ($videoId === '') {
            continue;
        }
        $title = trim((string) ($entry->title ?? ''));
        if (str_contains($title, '%')) {
            $decoded = urldecode($title);
            if ($decoded !== '') {
                $title = $decoded;
            }
        }
        if ($streamsOnly && (str_contains(strtolower($title), '#shorts') || str_contains(strtolower($title), '#short'))) {
            continue;
        }
        $published = trim((string) ($entry->published ?? ''));
        $thumb = '';
        if (isset($media->group->thumbnail)) {
            foreach ($media->group->thumbnail as $t) {
                $url = trim((string) ($t['url'] ?? ''));
                if ($url !== '') {
                    $thumb = $url;
                    break;
                }
            }
        }
        if ($thumb === '') {
            $thumb = 'https://i.ytimg.com/vi/' . rawurlencode($videoId) . '/hqdefault.jpg';
        }

        $videos[] = [
            'id' => $videoId,
            'title' => $title,
            'url' => 'https://www.youtube.com/watch?v=' . rawurlencode($videoId),
            'thumbnail' => $thumb,
            'publishedAt' => $published,
            'isLive' => false,
            'isStream' => $streamsOnly,
        ];
        if (count($videos) >= $scanLimit) {
            break;
        }
    }

    return $videos;
}

function mocker_yt_build_feed(array $cfg): array
{
    $channelId = mocker_yt_resolve_channel_id($cfg['handle'], $cfg['channelId']);
    if ($channelId === '') {
        return ['ok' => false, 'error' => 'channel'];
    }

    $videos = [];
    $source = '';
    $max = $cfg['maxVideos'];

    $videos = mocker_yt_fetch_streams_via_api($channelId, $max);
    if ($videos !== []) {
        $source = 'api_streams';
    }

    $streamsHtml = $videos === [] ? mocker_yt_http_get(mocker_yt_channel_tab_url($cfg['handle'], $channelId, 'streams')) : null;
    if ($streamsHtml !== null && $streamsHtml !== '') {
        $videos = mocker_yt_extract_videos_from_html($streamsHtml, $max, 'streams');
        if ($videos === []) {
            $videos = mocker_yt_extract_ids_from_html($streamsHtml, $max);
        }
        if ($videos !== []) {
            $source = 'streams';
            $videos = mocker_yt_enrich_titles($videos, $max);
        }
    }

    if ($videos === []) {
        $rssUrl = 'https://www.youtube.com/feeds/videos.xml?channel_id=' . rawurlencode($channelId);
        $xml = mocker_yt_http_get($rssUrl);
        if ($xml !== null && stripos(ltrim($xml), '<feed') !== false) {
            $videos = mocker_yt_parse_rss($xml, $max, true);
            if ($videos !== []) {
                $source = 'rss';
            }
        }
    }

    if ($videos === []) {
        $html = mocker_yt_http_get(mocker_yt_channel_tab_url($cfg['handle'], $channelId, 'videos'));
        if ($html !== null) {
            $videos = mocker_yt_extract_videos_from_html($html, $max, 'videos');
            if ($videos !== []) {
                $source = 'html';
                $videos = mocker_yt_enrich_titles($videos, $max);
            }
        }
    }

    $videos = mocker_yt_filter_stream_feed_videos($videos, $source);
    if ($videos === []) {
        return ['ok' => false, 'error' => 'rss'];
    }
    $videos = array_slice($videos, 0, $max);

    $liveId = mocker_yt_detect_live_id($channelId, $cfg['handle']);

    if ($liveId !== null) {
        $found = false;
        foreach ($videos as &$v) {
            if (($v['id'] ?? '') === $liveId) {
                $v['isLive'] = true;
                $found = true;
            }
        }
        unset($v);
        if (!$found) {
            array_unshift($videos, [
                'id' => $liveId,
                'title' => '',
                'url' => 'https://www.youtube.com/watch?v=' . rawurlencode($liveId),
                'thumbnail' => 'https://i.ytimg.com/vi/' . rawurlencode($liveId) . '/hqdefault.jpg',
                'publishedAt' => gmdate('c'),
                'isLive' => true,
            ]);
            if (count($videos) > $cfg['maxVideos']) {
                $videos = array_slice($videos, 0, $cfg['maxVideos']);
            }
        }
    }

    return [
        'ok' => true,
        'channelId' => $channelId,
        'channelUrl' => $cfg['channelUrl'],
        'liveVideoId' => $liveId,
        'videos' => $videos,
        'fetchedAt' => time(),
        'source' => $source,
    ];
}

$cfg = mocker_yt_config();
$channelId = mocker_yt_resolve_channel_id($cfg['handle'], $cfg['channelId']);

if ($cfg['channelUrl'] === '' && $cfg['handle'] === '' && $channelId === '') {
    mocker_yt_json_out(['ok' => false, 'error' => 'config']);
}

$cacheDir = dirname(__DIR__) . '/data/cache';
$cacheFile = $cacheDir . '/youtube_feed.json';
$cached = mocker_yt_read_json($cacheFile);
if (
    is_array($cached)
    && !empty($cached['ok'])
    && (int) ($cached['fetchedAt'] ?? 0) > time() - MOCKER_YT_CACHE_TTL
    && ($cached['channelId'] ?? '') === $channelId
) {
    mocker_yt_json_out($cached);
}

$feed = mocker_yt_build_feed($cfg);
if (!empty($feed['ok'])) {
    mocker_yt_persist_feed($feed);
    mocker_yt_json_out($feed);
}

$stale = mocker_yt_stale_feed($channelId);
if ($stale !== null) {
    mocker_yt_json_out($stale);
}

mocker_yt_json_out($feed, 503);
