<?php
declare(strict_types=1);

/**
 * lib/ibo.php — Capa de compatibilidad con la app IBO Pro Player (com.iptvrebr.glass).
 *
 * PANEL/ nació para otra app (contrato JSON limpio /api/device/config). Esta app,
 * en cambio, habla el contrato IBO: endpoints extensionless (auth/add/update/delete/
 * has_playlist), payload ofuscado {"data":"<scrambled>"} (scrambling t2.g del cliente)
 * y respuesta del /auth envuelta {"data":"<AppInfoModel JSON>"}.
 *
 * Esta capa traduce ese contrato al modelo de datos de PANEL/ (devices, dns, playlists,
 * settings), reutilizando lib/db, lib/helpers, lib/device y lib/settings.
 */

ini_set('display_errors', '1');
error_reporting(E_ALL);

require_once __DIR__ . '/db.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/device.php';
require_once __DIR__ . '/settings.php';
require_once __DIR__ . '/sport.php';

// Alfabeto del scrambling de la app (t2.g). 62 chars: a-z A-Z 0-9.
const IBO_ALPHA = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
// Caducidad "siempre activo" que se envía cuando el device no tiene sub_expire.
const IBO_ALWAYS_ACTIVE = '2099-12-31';
// Clave TMDB compartida del proyecto (la misma que ya viaja al cliente en tmdb_key
// del AppInfoModel; NO es secreta). Fallback para paneles cuyo settings.tmdb_api_key
// quedó vacío -> el backdrop de fotos funciona sin configurar nada por panel.
const IBO_TMDB_FALLBACK_KEY = 'b24683ba85cd4503335347d3785ddab7';
// Backdrops fijos (respaldo cuando el HOST del panel no llega a TMDB). El WebView
// los baja directo de image.tmdb.org, así que se ven aunque el server no tenga curl/outbound.
const IBO_TMDB_STATIC = [
    'https://image.tmdb.org/t/p/w1280/r013C8Me2bZ0pUi0OWJRh0h7MzT.jpg',
    'https://image.tmdb.org/t/p/w1280/1AVF2fAevpfi2HP6AEpptG1kg8R.jpg',
    'https://image.tmdb.org/t/p/w1280/c6BPbkO5Npt1OdwttAxCFo06wtH.jpg',
    'https://image.tmdb.org/t/p/w1280/A5Tz6ogGt4VV8NESG9oWVct5bo1.jpg',
    'https://image.tmdb.org/t/p/w1280/3pDfu71mrzSuFYyIfnKEjt8z4BC.jpg',
    'https://image.tmdb.org/t/p/w1280/m3Pom6pbD51bBv3syz8NMHda3fz.jpg',
    'https://image.tmdb.org/t/p/w1280/mCpwRayjXMFzKHbjbzc5JRKfq1O.jpg',
    'https://image.tmdb.org/t/p/w1280/qjTqY5coNiz6sVtPng40IzltsoN.jpg',
    'https://image.tmdb.org/t/p/w1280/vjMvFSmGUxEtqVdaZgvFee9XkZl.jpg',
    'https://image.tmdb.org/t/p/w1280/xMoXsOgF0hBP7iVj7ZPIrFtTeL3.jpg',
    'https://image.tmdb.org/t/p/w1280/cbyslfazMuDUGt4W6QcO3cKq8xj.jpg',
    'https://image.tmdb.org/t/p/w1280/t6O6XWelu27BD0OmIDCCufZZT6d.jpg',
    'https://image.tmdb.org/t/p/w1280/jLuGZc84MvPYCQomQg9DI72mstt.jpg',
    'https://image.tmdb.org/t/p/w1280/8Tfys3mDZVp4tNoH2ktm06a0Tau.jpg',
];

// Mapa: tema de PANEL (índice 0..5 del home glass) -> valor de tema que espera la
// app IBO ("1".."6"). El ThemeChooser del APK mapea el valor al layout de forma NO
// secuencial; este mapa se dedujo de las capturas (assets/themes/N.jpg = índice N):
//   0->1(home tiles) 1->4(hero+trending,list) 2->2(menu vert) 3->3(fondo) 4->5(filas,list) 5->6(hero+filas,list)
const IBO_THEME_MAP = ['0' => '1', '1' => '4', '2' => '2', '3' => '3', '4' => '5', '5' => '6'];
const IBO_DEFAULT_PANEL_THEME = '5'; // índice PANEL por defecto -> valor IBO 6 (verificado)

/* --------------------------------------------------------------------------
 *  Codec t2.g (des/ofuscado del payload de la app)  --  inverso exacto de t2.g.d
 * ------------------------------------------------------------------------ */
function ibo_decode(string $s): string
{
    $s = trim($s);
    if ($s === '') {
        return '';
    }
    if ($s[0] === '{' || $s[0] === '[') {
        return $s; // ya viene en claro (tests/curl)
    }
    $len = strlen($s);
    if ($len < 2) {
        return '';
    }
    $pos  = strpos(IBO_ALPHA, $s[$len - 2]); // índice de inserción de la basura
    $junk = strpos(IBO_ALPHA, $s[$len - 1]); // longitud de la basura
    if ($pos === false || $junk === false) {
        return '';
    }
    $body = substr($s, 0, $len - 2);
    $b64  = substr($body, 0, $pos) . substr($body, $pos + $junk);
    $json = base64_decode($b64, false);
    return $json === false ? '' : trim($json);
}

// Inverso de ibo_decode (imita t2.g.h del cliente). Lo usa el self-check.
function ibo_encode(string $json, int $junkLen, int $pos): string
{
    $b64 = base64_encode($json);
    $pos = max(0, min($pos, strlen($b64), 42));
    $junkLen = max(0, min($junkLen, 61));
    $junk = '';
    for ($i = 0; $i < $junkLen; $i++) {
        $junk .= IBO_ALPHA[random_int(0, 61)];
    }
    return substr($b64, 0, $pos) . $junk . substr($b64, $pos) . IBO_ALPHA[$pos] . IBO_ALPHA[$junkLen];
}

// Extrae el objeto del cuerpo POST ({"data":"<scrambled>"} o JSON plano).
function ibo_payload(): array
{
    $raw = file_get_contents('php://input') ?: '';
    $outer = json_decode($raw, true);
    $data = is_array($outer) && isset($outer['data']) ? (string) $outer['data'] : $raw;
    $obj = json_decode(ibo_decode($data), true);
    return is_array($obj) ? $obj : [];
}

// Anti-cache: Hostinger/LiteSpeed inyecta max-age=600 en PHP y la app (Volley)
// cacheaba el /auth 10 min -> "perdia" la playlist. Forzamos no-store.
function ibo_nocache(): void
{
    if (headers_sent()) {
        return;
    }
    header_remove('Cache-Control');
    header('Cache-Control: no-store, no-cache, private, must-revalidate, max-age=0', true);
    header('CDN-Cache-Control: no-store', true);
    header('Pragma: no-cache', true);
    header('Expires: 0', true);
}

function ibo_json_wrapped(string $innerJson): void
{
    ibo_nocache();
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['data' => $innerJson], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

function ibo_json(array $arr, int $code = 200): void
{
    http_response_code($code);
    ibo_nocache();
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($arr, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

/* --------------------------------------------------------------------------
 *  Identidad de dispositivo (IBO): app_device_id = base64(android_id).
 *  MAC derivada = primeros 12 hex del android_id (como los paneles IBO reales).
 * ------------------------------------------------------------------------ */
function ibo_derive_mac(string $appDeviceId): string
{
    $dec = base64_decode($appDeviceId, true);
    $src = ($dec !== false && $dec !== '') ? $dec : $appDeviceId;
    $hex = strtoupper(preg_replace('/[^0-9A-Fa-f]/', '', $src) ?? '');
    if (strlen($hex) >= 12) {
        return implode(':', str_split(substr($hex, 0, 12), 2));
    }
    // Fallback determinista si el android_id no da 12 hex.
    return implode(':', str_split(substr(strtoupper(md5($appDeviceId)), 0, 12), 2));
}

// Crea el device IBO (registered=1, siempre activo) si no existe, por MAC.
function ibo_get_or_create_device(string $appDeviceId): array
{
    $mac = ibo_derive_mac($appDeviceId);
    $dev = device_find_by_mac($mac);
    if ($dev === null) {
        $defTheme = settings_get('default_theme', IBO_DEFAULT_PANEL_THEME);
        db()->prepare(
            'INSERT INTO devices (mac, device_id, device_key, registered, theme, login_theme,
                sub_trial, parent_pin, parent_synced, last_seen, created_at)
             VALUES (?,?,?,1,?,?,0,"0000",1,?,?)'
        )->execute([$mac, $appDeviceId, device_unique_key(), (int)$defTheme, (int)$defTheme, now_iso(), now_iso()]);
        $dev = device_find_by_mac($mac);
    } else {
        db()->prepare('UPDATE devices SET device_id=COALESCE(NULLIF(?,""),device_id), last_seen=? WHERE id=?')
            ->execute([$appDeviceId, now_iso(), (int)$dev['id']]);
    }
    return $dev ?: [];
}

// Resuelve un device desde el payload (mac_address, ?mac= GET, o app_device_id).
function ibo_resolve_device(array $p): ?array
{
    if (!empty($p['mac_address'])) {
        $m = normalize_mac((string)$p['mac_address']);
        if ($m && ($d = device_find_by_mac($m))) {
            return $d;
        }
    }
    if (!empty($_GET['mac'])) {
        $m = normalize_mac((string)$_GET['mac']);
        if ($m && ($d = device_find_by_mac($m))) {
            return $d;
        }
    }
    if (!empty($p['app_device_id'])) {
        $d = device_find_by_mac(ibo_derive_mac((string)$p['app_device_id']));
        if ($d) {
            return $d;
        }
    }
    return null;
}

/* --------------------------------------------------------------------------
 *  Construcción de las respuestas
 * ------------------------------------------------------------------------ */

// urls[] del AppInfoModel = playlists del device en el formato que la app parsea
// (id/name/type/url/is_protected/expiry + username/password/playlist_name como ref).
function ibo_device_urls(int $deviceId): array
{
    $out = [];
    foreach (device_playlists($deviceId) as $p) {
        $u = (string)$p['username'];
        $pw = (string)$p['password'];
        if ($p['type'] === 'xtream') {
            // Reconstruye la URL Xtream get.php a partir del DNS base + credenciales.
            $full = rtrim($p['url'], '/') . "/get.php?username=$u&password=$pw&type=m3u_plus&output=ts";
            $type = 'xc';
        } else {
            $full = $p['url'];
            $type = $p['type'] !== '' ? $p['type'] : 'm3u';
        }
        $out[] = [
            'is_protected'  => $p['protected'] ? '1' : '0',
            'id'            => (string)$p['id'],
            'url'           => $full,
            'name'          => $p['name'],
            'playlist_name' => $p['name'],
            'type'          => $type,
            'username'      => $u,
            'password'      => $pw,
            'playlist_type' => $type,
            'expiry'        => '',
            'created_at'    => '2023-03-26 16:42:48',
            'updated_at'    => '2023-03-26 16:42:48',
        ];
    }
    return $out;
}

// portals[] = servidores DNS globales (el desplegable de "añadir playlist").
function ibo_portals(): array
{
    $out = [];
    foreach (db()->query('SELECT id,title,url FROM dns ORDER BY id')->fetchAll() as $s) {
        $out[] = [
            'id'           => (string)$s['id'],
            'name'         => (string)$s['title'],
            'type'         => 'server',
            'url'          => (string)$s['url'],
            'is_protected' => '0',
            'expiry'       => '',
        ];
    }
    return $out;
}

function ibo_qr_url(): string
{
    $scheme = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') ? 'https' : 'http';
    $host = (string)($_SERVER['HTTP_HOST'] ?? 'localhost');
    $base = rtrim(str_replace('\\', '/', dirname((string)($_SERVER['SCRIPT_NAME'] ?? ''))), '/');
    $q = trim((string)settings_get('qr_url', ''));
    if ($q === '' || preg_match('/\.(png|jpe?g|webp|gif)(\?.*)?$/i', $q)) {
        return "$scheme://$host$base/qr.php?data=";
    }
    return $q;
}

// Base pública del panel (scheme+host+ruta) calculada del request. Igual criterio
// que ibo_qr_url: dirname de SCRIPT_NAME (ibo.php), sirve bajo cualquier prefijo.
function ibo_panel_base(): string
{
    $scheme = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') ? 'https' : 'http';
    $host = (string)($_SERVER['HTTP_HOST'] ?? 'localhost');
    $base = rtrim(str_replace('\\', '/', dirname((string)($_SERVER['SCRIPT_NAME'] ?? ''))), '/');
    return "$scheme://$host$base";
}

// home_url1 = fondo del home. Los temas home_2..home_6 tienen un WebView (hax_webview1)
// que HomeBackdropLoader carga con este valor; si viene vacío, el fondo sale EN BLANCO
// (bug de los rebrands). Sin configurar, apuntamos al backdrop propio del panel
// (webview.php?fetch=home) que rota fotos de TMDB -> nunca queda en blanco.
function ibo_home_url(): string
{
    $u = trim((string)settings_get('home_url1', settings_get('def_home_url', '')));
    return $u !== '' ? $u : ibo_panel_base() . '/webview.php?fetch=home';
}

// AppInfoModel completo que la app IBO espera dentro de {"data": ...} del /auth.
function ibo_build_app_info(array $d): array
{
    $panelTheme = (string)($d['theme'] ?? '');
    if ($panelTheme === '' || !array_key_exists($panelTheme, IBO_THEME_MAP)) {
        $panelTheme = (string)settings_get('default_theme', IBO_DEFAULT_PANEL_THEME);
    }
    $iboTheme = IBO_THEME_MAP[$panelTheme] ?? '6';

    $expire = trim((string)($d['sub_expire'] ?? '')) !== '' ? (string)$d['sub_expire'] : IBO_ALWAYS_ACTIVE;

    return [
        'mac_registered' => true,
        'mac_address'    => (string)$d['mac'],
        'device_key'     => (string)($d['device_key'] ?? ''),
        'expire_date'    => $expire,
        'is_trial'       => (int)($d['sub_trial'] ?? 0),
        'lock'           => (int)($d['locked'] ?? 0),
        'price'          => (string)($d['price'] ?? settings_get('price', '0')),
        'plan_id'        => (string)($d['plan_id'] ?? ''),
        'theme'          => $iboTheme,
        'home_url1'      => ibo_home_url(),
        'subtitle_url'   => (string)settings_get('subtitle_url', ''),
        'note_title'     => (string)($d['noti_title'] ?? settings_get('def_noti_title', '')),
        'note_content'   => (string)($d['noti_content'] ?? settings_get('def_noti_content', '')),
        'parent_control' => (string)($d['parent_pin'] ?? '0000'),
        'parent_synced'  => (int)($d['parent_synced'] ?? 1),
        'pin'            => (string)($d['parent_pin'] ?? '0000'),
        'app_version'    => (string)settings_get('app_version', '1.0'),
        'apk_link'       => (string)settings_get('apk_url', settings_get('apk_link', '')),
        'qr_url'         => ibo_qr_url(),
        'urls'           => ibo_device_urls((int)$d['id']),
        'portals'        => ibo_portals(),
        'android_testing' => ['ibo' => false, 'vu' => false],
    ];
}

/* --------------------------------------------------------------------------
 *  Endpoints IBO
 * ------------------------------------------------------------------------ */
function ibo_dispatch(string $route): void
{
    switch ($route) {
        case 'auth':                  ibo_auth();            break;
        case 'add':
        case 'update':                ibo_upsert_playlist(); break;
        case 'delete':                ibo_delete_playlist(); break;
        case 'parent-control/update': ibo_parent_control();  break;
        case 'has_playlist':          ibo_has_playlist();    break;
        case 'vpns':                  ibo_vpns();            break;
        case 'webview':               ibo_webview();         break;
        default:                      ibo_json(['status' => false, 'error' => 'unknown'], 404);
    }
}

function ibo_auth(): void
{
    $p = ibo_payload();
    $appDeviceId = trim((string)($p['app_device_id'] ?? ''));
    if ($appDeviceId === '') {
        ibo_json(['mac_registered' => false, 'error' => 'missing device id'], 400);
    }
    $d = ibo_get_or_create_device($appDeviceId);
    ibo_json_wrapped(json_encode(ibo_build_app_info($d), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}

// Parsea la URL que manda la app -> [type, dns_base|full_url, username, password].
function ibo_parse_playlist_url(string $url): array
{
    $q = parse_url($url, PHP_URL_QUERY);
    $user = $pass = '';
    if ($q) { parse_str($q, $qs); $user = (string)($qs['username'] ?? ''); $pass = (string)($qs['password'] ?? ''); }
    if ($user !== '' || strpos($url, 'player_api.php') !== false || strpos($url, 'get.php') !== false) {
        // Xtream: guardamos solo el DNS base + credenciales (modelo de PANEL).
        $parts = parse_url($url);
        $base = ($parts['scheme'] ?? 'http') . '://' . ($parts['host'] ?? '');
        if (!empty($parts['port'])) { $base .= ':' . $parts['port']; }
        return ['xtream', $base, $user, $pass];
    }
    return ['m3u', $url, '', ''];
}

function ibo_upsert_playlist(): void
{
    $p = ibo_resolve_device_or_create($p2 = ibo_payload());
    if (!$p) {
        ibo_json(['success' => 0, 'message' => 'device not found']);
    }
    $devId = (int)$p['id'];
    $url  = trim((string)($p2['playlist_url'] ?? $p2['url'] ?? ''));
    $name = trim((string)($p2['playlist_name'] ?? $p2['name'] ?? '')) ?: 'Playlist';
    $pid  = (int)($p2['playlist_id'] ?? $p2['id'] ?? 0);
    if ($url === '') {
        ibo_json(['success' => 0, 'message' => 'missing url']);
    }
    [$type, $storeUrl, $user, $pass] = ibo_parse_playlist_url($url);

    // Edit: si playlist_id es un id de fila real de este device -> UPDATE por id.
    $rowId = 0;
    if ($pid > 0) {
        $st = db()->prepare('SELECT id FROM playlists WHERE id=? AND device_id=?');
        $st->execute([$pid, $devId]);
        $rowId = (int)($st->fetchColumn() ?: 0);
    }
    // Si no, dedup por (device, url base + user) para no duplicar la misma cuenta.
    if ($rowId === 0) {
        $st = db()->prepare('SELECT id FROM playlists WHERE device_id=? AND url=? AND COALESCE(username,"")=?');
        $st->execute([$devId, $storeUrl, $user]);
        $rowId = (int)($st->fetchColumn() ?: 0);
    }

    if ($rowId > 0) {
        db()->prepare('UPDATE playlists SET name=?,type=?,url=?,username=?,password=? WHERE id=? AND device_id=?')
            ->execute([$name, $type, $storeUrl, $user, $pass, $rowId, $devId]);
    } else {
        db()->prepare('INSERT INTO playlists(device_id,name,type,url,username,password,protected,sort) VALUES(?,?,?,?,?,?,0,0)')
            ->execute([$devId, $name, $type, $storeUrl, $user, $pass]);
        $rowId = (int)db()->lastInsertId();
    }
    ibo_json(['success' => 1, 'id' => (string)$rowId, 'name' => $name, 'url' => $url, 'expiry' => '']);
}

// Igual que ibo_resolve_device pero crea el device si no existe (por si /add llega
// antes que /auth). Recibe el payload YA leído.
function ibo_resolve_device_or_create(array $p): ?array
{
    $d = ibo_resolve_device($p);
    if ($d) {
        return $d;
    }
    if (!empty($p['app_device_id'])) {
        return ibo_get_or_create_device((string)$p['app_device_id']);
    }
    if (!empty($p['mac_address'])) {
        $mac = normalize_mac((string)$p['mac_address']);
        if ($mac) {
            db()->prepare('INSERT OR IGNORE INTO devices(mac,device_id,device_key,registered,theme,login_theme,sub_trial,parent_pin,parent_synced,last_seen,created_at) VALUES(?,?,?,1,?,?,0,"0000",1,?,?)')
                ->execute([$mac, $mac, device_unique_key(), (int)settings_get('default_theme', IBO_DEFAULT_PANEL_THEME), (int)settings_get('default_theme', IBO_DEFAULT_PANEL_THEME), now_iso(), now_iso()]);
            return device_find_by_mac($mac);
        }
    }
    return null;
}

function ibo_delete_playlist(): void
{
    $p = ibo_payload();
    $d = ibo_resolve_device($p);
    $id = (int)($p['playlist_id'] ?? $p['id'] ?? 0);
    if ($d && $id > 0) {
        db()->prepare('DELETE FROM playlists WHERE id=? AND device_id=?')->execute([$id, (int)$d['id']]);
    }
    ibo_json(['status' => true, 'success' => 1]);
}

function ibo_parent_control(): void
{
    $p = ibo_payload();
    $d = ibo_resolve_device($p);
    if ($d) {
        db()->prepare('UPDATE devices SET parent_pin=?, parent_synced=1 WHERE id=?')
            ->execute([(string)($p['pin'] ?? $p['parent_control'] ?? $d['parent_pin']), (int)$d['id']]);
    }
    ibo_json(['status' => true]);
}

// La app reinicia si has_playlist > glassInitialCount = maxId(urls[].id). Devolver
// el MAX(id) de las playlists DE ESTE device -> recarga cuando falta y CONVERGE.
function ibo_has_playlist(): void
{
    ibo_nocache();
    header('Content-Type: text/plain');
    $max = 0;
    $d = ibo_resolve_device([]);
    if ($d) {
        $st = db()->prepare('SELECT COALESCE(MAX(id),0) FROM playlists WHERE device_id=?');
        $st->execute([(int)$d['id']]);
        $max = (int)$st->fetchColumn();
    }
    echo (string)$max;
    exit;
}

function ibo_vpns(): void
{
    ibo_nocache();
    header('Content-Type: application/json; charset=utf-8');
    $out = [];
    foreach (db()->query('SELECT name,ovpn,username,password FROM vpn_servers WHERE enabled=1 ORDER BY id')->fetchAll() as $v) {
        $out[] = [
            'name'     => (string)$v['name'],
            'ovpn'     => (string)$v['ovpn'],
            'username' => (string)$v['username'],
            'password' => (string)$v['password'],
        ];
    }
    echo json_encode(['vpns' => $out], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

// Valida un color de la config (#rgb..#rrggbbaa); si no, usa el fallback.
function ibo_safe_color(?string $c, string $fallback): string
{
    $c = trim((string)$c);
    return preg_match('/^#[0-9a-fA-F]{3,8}$/', $c) === 1 ? $c : $fallback;
}

// Etiqueta legible de la fecha: HOY / MAÑANA / "Vie 11 Jul" (en español, UTC).
function ibo_date_label(string $d, string $today, string $tomorrow): string
{
    if ($d === $today)    { return 'HOY'; }
    if ($d === $tomorrow) { return 'MAÑANA'; }
    $ts = strtotime($d . ' 00:00:00 UTC');
    if ($ts === false) { return $d; }
    $dow = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
    $mon = ['', 'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];
    return $dow[(int)gmdate('w', $ts)] . ' ' . gmdate('j', $ts) . ' ' . $mon[(int)gmdate('n', $ts)];
}
// Idioma del dispositivo desde Accept-Language (o ?lang=xx). Default: inglés.
function ibo_lang(array $supported): string
{
    if (isset($_GET['lang']) && isset($supported[strtolower((string)$_GET['lang'])])) {
        return strtolower((string)$_GET['lang']);
    }
    foreach (explode(',', (string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')) as $part) {
        $code = strtolower(substr(trim($part), 0, 2));
        if ($code !== '' && isset($supported[$code])) {
            return $code;
        }
    }
    return 'en';
}
// Backdrops de TMDB resueltos SERVER-SIDE (URLs de imagen completas). Antes se
// hacia fetch() dentro del WebView y en el WebView de la app NO corre -> solo salia
// el degradado. Aca lo resuelve PHP y las imagenes van embebidas; el WebView solo
// tiene que CARGAR imagenes (eso si funciona). Cachea 6h en settings para no pegarle
// a TMDB en cada arranque. [] si TMDB no responde (queda el degradado de respaldo).
function ibo_tmdb_backdrops(int $limit = 15): array
{
    // Cache simple en settings (evita una llamada por cada /home).
    $cached = json_decode((string)settings_get('home_bd_cache', ''), true);
    $ts = (int)settings_get('home_bd_cache_ts', '0');
    if (is_array($cached) && $cached && (time() - $ts) < 21600) {
        return array_slice($cached, 0, $limit);
    }

    $key = trim((string)settings_get('tmdb_api_key', ''));
    if ($key === '') {
        $key = IBO_TMDB_FALLBACK_KEY; // panel sin tmdb configurado -> clave del proyecto
    }
    $url  = 'https://api.themoviedb.org/3/trending/all/week?api_key=' . urlencode($key);
    $body = '';
    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 8,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_HTTPHEADER     => ['Accept: application/json'],
        ]);
        $body = (string)curl_exec($ch);
        curl_close($ch);
    }
    if ($body === '') {
        $ctx  = stream_context_create(['http' => ['timeout' => 8, 'header' => "Accept: application/json\r\n"]]);
        $body = (string)@file_get_contents($url, false, $ctx);
    }
    $data = json_decode($body, true);
    $out  = [];
    foreach ((array)($data['results'] ?? []) as $m) {
        $bp = (string)($m['backdrop_path'] ?? '');
        if ($bp !== '') {
            $out[] = 'https://image.tmdb.org/t/p/w1280' . $bp;
        }
    }

    // Respaldo FIJO: si el host del panel NO llega a TMDB (outbound bloqueado / sin
    // curl), igual mostramos fotos. El WebView las baja directo de image.tmdb.org
    // (que sí alcanza: la app tiene internet). Nunca queda en gris.
    if (!$out) {
        $out = IBO_TMDB_STATIC;
    }

    // Cachea el resultado (fresco o estático) 6h para no reintentar en cada /home.
    settings_set('home_bd_cache', json_encode($out, JSON_UNESCAPED_SLASHES));
    settings_set('home_bd_cache_ts', (string)time());
    return array_slice($out, 0, $limit);
}

// Fondo del home (hax_webview1 de los temas home_2..home_6). Muestra fotos (backdrops
// de TMDB) rotando; las URLs van EMBEBIDAS desde PHP (server-side), no por fetch del
// WebView. Degradado de respaldo si TMDB no dio nada -> nunca en blanco.
function ibo_home_backdrop(): void
{
    header('Content-Type: text/html; charset=utf-8');
    $pics   = ibo_tmdb_backdrops(15);
    $picsJs = json_encode(array_values($pics), JSON_UNESCAPED_SLASHES);

    echo '<!doctype html><html><head><meta charset="utf-8"><!--bdv3-->'
       . '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">'
       . '<style>'
       . 'html,body{margin:0;height:100%;background:#0b0716;overflow:hidden}'
       . '.fb{position:fixed;inset:0;background:radial-gradient(130% 85% at 72% 12%,rgba(124,58,237,.30),transparent 60%),#0b0716}'
       . '.bd{position:fixed;inset:0;background-size:cover;background-position:center;opacity:0;transition:opacity 1.4s ease}'
       . '.bd.on{opacity:1}'
       . '.scrim{position:fixed;inset:0;pointer-events:none;'
       . 'background:radial-gradient(120% 90% at 70% 18%,rgba(11,7,22,0) 42%,rgba(11,7,22,.5) 100%),'
       . 'linear-gradient(180deg,rgba(11,7,22,0) 55%,rgba(11,7,22,.62) 100%)}'
       . '</style></head><body>'
       . '<div class="fb"></div><div class="bd" id="a"></div><div class="bd" id="b"></div><div class="scrim"></div>'
       . '<script>(function(){'
       . 'var PICS=' . $picsJs . ';'
       . 'var a=document.getElementById("a"),b=document.getElementById("b"),cur=a,nxt=b,i=0;'
       . 'function show(u){var im=new Image();im.onload=function(){nxt.style.backgroundImage="url(\'"+u+"\')";'
       . 'nxt.classList.add("on");cur.classList.remove("on");var t=cur;cur=nxt;nxt=t};im.src=u;}'
       . 'if(PICS.length){show(PICS[0]);i=1;'
       . 'if(PICS.length>1){setInterval(function(){show(PICS[i%PICS.length]);i++;},9000);}}'
       . '})();</script></body></html>';
    exit;
}

// SportsWebActivity carga esto en un WebView: _panelUrl + "webview.php?fetch=sport".
// GUIA de 3 paneles (Deportes | Ligas | Eventos) al estilo de la app de referencia:
// buscadores, iconos, logos reales y navegable con control remoto (D-pad). Estilo
// glass morado de la app IBO GLASS. Datos reales multi-deporte (import_sport.php),
// multiidioma segun el dispositivo (default ingles). Responsive: en pantalla angosta
// los paneles se apilan.
function ibo_webview(): void
{
    ibo_nocache();

    // Fondo del home (home_url1): backdrop con fotos rotando. Los temas home_2..home_6
    // lo cargan en su WebView; sin esto sale en blanco en los rebrands.
    if (($_GET['fetch'] ?? '') === 'home') {
        ibo_home_backdrop();
        return;
    }

    $L = [
        'en' => [
            'ss'=>'Search Sports','sl'=>'Search League','se'=>'Search Events','allsports'=>'All Sports',
            'season'=>'Season','vs'=>'vs','live'=>'LIVE','final'=>'Final','empty'=>'No events scheduled',
            'wd'=>['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
            'mon'=>['','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
            'cat'=>['soccer'=>'Football','basketball'=>'Basketball','icehockey'=>'Ice Hockey','baseball'=>'Baseball','amfootball'=>'Am. Football','motorsport'=>'Motorsport','fighting'=>'Boxing/MMA','tennis'=>'Tennis','golf'=>'Golf','rugby'=>'Rugby','cricket'=>'Cricket','volleyball'=>'Volleyball'],
        ],
        'es' => [
            'ss'=>'Buscar deporte','sl'=>'Buscar liga','se'=>'Buscar evento','allsports'=>'Todos los deportes',
            'season'=>'Temporada','vs'=>'vs','live'=>'EN VIVO','final'=>'Final','empty'=>'Sin eventos programados',
            'wd'=>['Dom','Lun','Mar','Mié','Jue','Vie','Sáb'],
            'mon'=>['','Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'],
            'cat'=>['soccer'=>'Fútbol','basketball'=>'Baloncesto','icehockey'=>'Hockey','baseball'=>'Béisbol','amfootball'=>'Fútbol Am.','motorsport'=>'Motor','fighting'=>'Boxeo/MMA','tennis'=>'Tenis','golf'=>'Golf','rugby'=>'Rugby','cricket'=>'Críquet','volleyball'=>'Vóleibol'],
        ],
        'pt' => [
            'ss'=>'Buscar esporte','sl'=>'Buscar liga','se'=>'Buscar evento','allsports'=>'Todos os esportes',
            'season'=>'Temporada','vs'=>'vs','live'=>'AO VIVO','final'=>'Encerrado','empty'=>'Sem eventos programados',
            'wd'=>['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'],
            'mon'=>['','Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'],
            'cat'=>['soccer'=>'Futebol','basketball'=>'Basquete','icehockey'=>'Hóquei','baseball'=>'Beisebol','amfootball'=>'Fut. Americano','motorsport'=>'Automobilismo','fighting'=>'Boxe/MMA','tennis'=>'Tênis','golf'=>'Golfe','rugby'=>'Rugby','cricket'=>'Críquete','volleyball'=>'Vôlei'],
        ],
    ];
    $lang = ibo_lang($L);
    $t = $L[$lang];
    $catName = static function (string $k) use ($t): string {
        return $t['cat'][$k] ?? ($k === '' ? ($t['cat']['soccer'] ?? 'Sport') : ucfirst($k));
    };

    $days   = max(1, (int)(sport_block()['days'] ?? 7));
    $events = !empty(sport_block()['enabled']) ? sport_events_window($days) : [];

    // Árbol deporte -> liga -> eventos.
    $tree = [];
    foreach ($events as $e) {
        $sk = $e['category'] !== '' ? $e['category'] : 'soccer';
        $lg = $e['league'] !== '' ? $e['league'] : $catName($sk);
        if (!isset($tree[$sk])) { $tree[$sk] = []; }
        if (!isset($tree[$sk][$lg])) { $tree[$sk][$lg] = ['badge' => '', 'events' => []]; }
        if ($tree[$sk][$lg]['badge'] === '' && $e['league_badge'] !== '') { $tree[$sk][$lg]['badge'] = $e['league_badge']; }
        $tree[$sk][$lg]['events'][] = [
            'home' => $e['home'], 'away' => $e['away'], 'date' => $e['date'], 'time' => $e['time'],
            'hb' => $e['home_badge'], 'ab' => $e['away_badge'],
            'ch' => $e['channels'], 'st' => $e['status'] ?? '', 'sc' => $e['score'] ?? '',
        ];
    }
    $sports = [];
    foreach ($tree as $sk => $lgs) {
        $leagues = [];
        $cnt = 0;
        foreach ($lgs as $ln => $ld) {
            $leagues[] = ['name' => $ln, 'badge' => $ld['badge'], 'events' => $ld['events']];
            $cnt += count($ld['events']);
        }
        usort($leagues, static fn ($a, $b) => count($b['events']) <=> count($a['events']));
        $sports[] = ['key' => $sk, 'name' => $catName($sk), 'count' => $cnt, 'leagues' => $leagues];
    }
    usort($sports, static fn ($a, $b) => $b['count'] <=> $a['count']);

    $jflags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
    $json = json_encode(['sports' => $sports], $jflags);
    $tj   = json_encode($t, $jflags);
    $esc  = static fn ($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');

    header('Content-Type: text/html; charset=utf-8');

    $css = <<<'CSS'
:root{--bg:#0b0716;--fg:#ece9f8;--accent:#b98cff;--accent2:#7c3aed;--muted:rgba(236,233,248,.56);
--glass:linear-gradient(160deg,rgba(255,255,255,.075),rgba(255,255,255,.022));
--stroke:rgba(255,255,255,.10);--stroke2:rgba(185,140,255,.34)}
*{box-sizing:border-box;margin:0;padding:0}
html{font-size:clamp(13px,0.42vw + 0.35vh,17px);-webkit-text-size-adjust:100%}
body{color:var(--fg);font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;line-height:1.25;
background:radial-gradient(125% 75% at 50% -12%,rgba(124,58,237,.16),transparent 55%),#0b0716;
background-attachment:fixed;height:100dvh;overflow:hidden}
.app{display:grid;grid-template-columns:minmax(0,.82fr) minmax(0,1fr) minmax(0,1.75fr);
gap:clamp(.4rem,1vw,.85rem);height:100dvh;padding:clamp(.4rem,1.4vw,1rem)}
.pane{background:var(--glass);backdrop-filter:blur(16px) saturate(1.3);-webkit-backdrop-filter:blur(16px) saturate(1.3);
border:1px solid var(--stroke);border-radius:1rem;display:flex;flex-direction:column;min-height:0;overflow:hidden}
.search{margin:.55rem;padding:.6em .9em;font:inherit;font-size:clamp(.8rem,1.15vw,1rem);color:var(--fg);
background:rgba(255,255,255,.05);border:1px solid var(--stroke);border-radius:.7rem;outline:none}
.search:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(185,140,255,.4)}
.search::placeholder{color:var(--muted)}
.list{overflow-y:auto;flex:1;padding:.3rem .5rem .6rem;display:flex;flex-direction:column;gap:.25rem}
.row{display:flex;align-items:center;gap:.65rem;padding:.58rem .7rem;border-radius:.7rem;cursor:pointer;outline:none;
border:1px solid transparent;scroll-margin:.7rem}
.row:focus,.row:hover{background:rgba(255,255,255,.07);border-color:var(--stroke2)}
.row.sel{background:linear-gradient(135deg,rgba(124,58,237,.55),rgba(185,140,255,.26));border-color:transparent}
.row.empty{opacity:.5}.row.empty.sel,.row.empty:focus{opacity:1}
.row .ic{width:1.6rem;height:1.6rem;flex-shrink:0;display:flex;align-items:center;justify-content:center;color:var(--accent)}
.row .ic svg{width:100%;height:100%}.row .ic img{width:100%;height:100%;object-fit:contain}
.row .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:700;font-size:clamp(.85rem,1.15vw,1.05rem)}
.row .cnt{color:var(--muted);font-size:.82em;font-weight:700;font-variant-numeric:tabular-nums}
.row.sel .cnt{color:rgba(255,255,255,.85)}
.detail{padding:clamp(.9rem,2vw,1.5rem) 1rem;text-align:center;border-bottom:1px solid var(--stroke)}
.detail img{height:clamp(3.4rem,8vw,5.6rem);width:auto;object-fit:contain;margin:.4rem auto;display:block;filter:drop-shadow(0 3px 8px rgba(0,0,0,.5))}
.detail h2{font-size:clamp(1.05rem,1.8vw,1.5rem);font-weight:800;text-wrap:balance}
.detail .sub{color:var(--muted);font-size:clamp(.76rem,1.05vw,.92rem);margin-top:.12rem}
.detail .season{color:var(--accent);font-weight:700}
.ev{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:.7rem;padding:.62rem .8rem;border-radius:.8rem;
border:1px solid transparent;outline:none;cursor:pointer;scroll-margin:.7rem}
.ev:focus,.ev:hover{background:rgba(255,255,255,.07);border-color:var(--stroke2)}
.ev .eb{width:clamp(1.8rem,3.2vw,2.5rem);height:clamp(1.8rem,3.2vw,2.5rem);flex-shrink:0;display:flex;align-items:center;justify-content:center}
.ev .eb img{width:100%;height:100%;object-fit:contain}
.ev .mid{text-align:center;min-width:0}
.ev .mt{font-weight:800;font-size:clamp(.9rem,1.3vw,1.15rem);overflow-wrap:break-word}
.ev .dt{color:var(--muted);font-size:.8em;margin-top:.15rem}
.stx{font-weight:800;font-size:.92em;padding:.08em .5em;border-radius:.4rem;white-space:nowrap}
.stx.live{color:#fff;background:linear-gradient(135deg,var(--accent2),var(--accent));box-shadow:0 0 .6rem rgba(185,140,255,.5)}
.stx.fin{color:var(--muted);border:1px solid var(--stroke)}
.ev .ch{display:flex;flex-wrap:wrap;gap:.3rem;justify-content:center;margin-top:.35rem}
.ev .ch span{font-size:.72em;font-weight:600;padding:.16em .55em;border-radius:.4rem;background:rgba(255,255,255,.06);border:1px solid var(--stroke);color:rgba(236,233,248,.82)}
.empty{padding:2.5rem 1rem;text-align:center;color:var(--muted)}
/* Celular: se MANTIENEN los 3 paneles (columnas fluidas), solo se ajusta el espaciado. */
@media(max-width:640px){.app{gap:.3rem;padding:.3rem}.search{margin:.35rem;padding:.5em .6em}
.list{padding:.2rem .25rem .4rem}.row{padding:.45rem .5rem;gap:.4rem}.row .ic{width:1.3rem;height:1.3rem}
.ev{padding:.5rem .5rem;gap:.4rem}.detail{padding:.7rem .5rem}.detail img{height:2.6rem}}
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
CSS;

    $js = <<<'JS'
(function(){
var D=window.__SPORTS__||{sports:[]},T=window.__T__||{};
var sports=D.sports||[];
var $=function(i){return document.getElementById(i)};
var c1=$("c1"),c2=$("c2"),c3=$("c3"),c3h=$("c3h"),s1=$("s1"),s2=$("s2"),s3=$("s3");
s1.placeholder=T.ss||"";s2.placeholder=T.sl||"";s3.placeholder=T.se||"";
var BALL='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="M12 7.2l4.3 3.1-1.6 5h-5.4l-1.6-5z"/></svg>';
var ICON={all:'<svg viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="7.5" height="7.5" rx="1.6"/><rect x="13.5" y="3" width="7.5" height="7.5" rx="1.6"/><rect x="3" y="13.5" width="7.5" height="7.5" rx="1.6"/><rect x="13.5" y="13.5" width="7.5" height="7.5" rx="1.6"/></svg>',
soccer:BALL,
basketball:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3v18M5.6 5.6c3.4 3.4 3.4 9.4 0 12.8M18.4 5.6c-3.4 3.4-3.4 9.4 0 12.8"/></svg>',
baseball:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="M6.8 5c2.2 3.2 2.2 10.8 0 14M17.2 5c-2.2 3.2-2.2 10.8 0 14"/></svg>',
amfootball:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><ellipse cx="12" cy="12" rx="9" ry="5.6"/><path d="M8.5 12h7M12 9.7v4.6"/></svg>',
tennis:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="M5.2 6.4c4 2.4 4 8.8 0 11.2M18.8 6.4c-4 2.4-4 8.8 0 11.2"/></svg>',
motorsport:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M5 21V4h13l-2.4 4L18 12H5"/></svg>',
icehockey:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 4l7 13"/><ellipse cx="14.5" cy="18.5" rx="5" ry="1.8"/></svg>',
fighting:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M7 8a3 3 0 0 1 3-3h4a4 4 0 0 1 4 4v3a4 4 0 0 1-4 4H9a3 3 0 0 1-3-3zM7 10H5a2 2 0 0 0 0 4h2"/></svg>',
golf:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M9 21V3l8 3.2L9 9.4"/><ellipse cx="11" cy="21" rx="6" ry="1.6"/></svg>',
rugby:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><ellipse cx="12" cy="12" rx="9" ry="5.6" transform="rotate(-40 12 12)"/><path d="M10 14l4-4M11 11.5l1.6 1.6M12.4 10.1l1.6 1.6"/></svg>'};
function icon(k){return ICON[k]||BALL}
function esc(s){var d=document.createElement("div");d.textContent=s==null?"":String(s);return d.innerHTML}
function el(t,c){var e=document.createElement(t);if(c)e.className=c;return e}
var curSport=null,curLeague=null;
function sportByKey(k){for(var i=0;i<sports.length;i++)if(sports[i].key===k)return sports[i];return null}
function allLeagues(){var a=[];sports.forEach(function(s){s.leagues.forEach(function(l){a.push(l)})});a.sort(function(x,y){return y.events.length-x.events.length});return a}
function currentLeagues(){if(curSport==="all")return allLeagues();var s=sportByKey(curSport);return s?s.leagues:[]}
function curLeagueObj(){var ls=currentLeagues();for(var i=0;i<ls.length;i++)if(ls[i].name===curLeague)return ls[i];return null}
function sportNameOfLeague(l){for(var i=0;i<sports.length;i++){var s=sports[i];for(var j=0;j<s.leagues.length;j++)if(s.leagues[j].name===l.name)return s.name}return ""}
function season(evs){if(!evs.length)return "";var y=parseInt(evs[0].date.slice(0,4),10),m=parseInt(evs[0].date.slice(5,7),10);if(!y)return "";var a=m>=7?y:y-1;return (T.season||"")+" "+a+"-"+(a+1)}
function dt(e){var d=new Date((e.date||"")+"T"+(e.time||"00:00")+":00Z");if(isNaN(d.getTime()))return (e.date||"")+(e.time?" "+e.time:"");
var wd=(T.wd&&T.wd[d.getDay()])||"",mn=(T.mon&&T.mon[d.getMonth()+1])||"",hh=("0"+d.getHours()).slice(-2),mm=("0"+d.getMinutes()).slice(-2);
return wd+" "+d.getDate()+" "+mn+"  ·  "+hh+":"+mm}

var SPORT_ORDER=["soccer","basketball","baseball","amfootball","icehockey","motorsport","fighting","tennis","golf","rugby","cricket","volleyball"];
function renderSports(){
 c1.innerHTML="";var total=0,byk={};sports.forEach(function(s){total+=s.count;byk[s.key]=s});
 var order=SPORT_ORDER.slice();sports.forEach(function(s){if(order.indexOf(s.key)<0)order.push(s.key)});
 var rows=[{key:"all",name:T.allsports||"All",count:total}];
 order.forEach(function(k){var s=byk[k];rows.push({key:k,name:(T.cat&&T.cat[k])||k,count:s?s.count:0})});
 rows.forEach(function(s){
  var r=el("div","row"+(curSport===s.key?" sel":"")+(s.count?"":" empty"));r.tabIndex=0;r.setAttribute("data-k",s.key);
  r.innerHTML='<span class="ic">'+icon(s.key)+'</span><span class="nm">'+esc(s.name)+'</span><span class="cnt">'+s.count+'</span>';
  r.addEventListener("focus",function(){selectSport(s.key)});
  r.addEventListener("click",function(){selectSport(s.key)});
  c1.appendChild(r);
 });
}
function selectSport(key){
 curSport=key;
 [].slice.call(c1.children).forEach(function(r){r.classList.toggle("sel",r.getAttribute("data-k")===key)});
 renderLeagues("");
}
function renderLeagues(filter){
 c2.innerHTML="";var ls=currentLeagues(),f=(filter||"").toLowerCase(),first=null;
 ls.forEach(function(l){
  if(f&&l.name.toLowerCase().indexOf(f)<0)return;
  var r=el("div","row");r.tabIndex=0;r.setAttribute("data-l",l.name);r._l=l;
  var ic=l.badge?'<img src="'+esc(l.badge)+'" alt="" onerror="this.remove()">':icon("soccer");
  r.innerHTML='<span class="ic">'+ic+'</span><span class="nm">'+esc(l.name)+'</span><span class="cnt">'+l.events.length+'</span>';
  r.addEventListener("focus",function(){selectLeague(l)});
  r.addEventListener("click",function(){selectLeague(l)});
  c2.appendChild(r);if(!first)first=r;
 });
 if(first){selectLeague(first._l)}else{c3h.innerHTML="";c3.innerHTML='<div class="empty">'+esc(T.empty||"")+'</div>'}
}
function markLeague(n){[].slice.call(c2.children).forEach(function(r){r.classList.toggle("sel",r.getAttribute("data-l")===n)})}
function selectLeague(l){
 if(!l)return;curLeague=l.name;markLeague(l.name);
 var head=(l.badge?'<img src="'+esc(l.badge)+'" alt="" onerror="this.remove()">':'')
  +'<h2>'+esc(l.name)+'</h2><div class="sub">'+esc(sportNameOfLeague(l))+'</div>'
  +'<div class="sub season">'+esc(season(l.events))+'</div>';
 c3h.innerHTML=head;renderEvents("");
}
function renderEvents(filter){
 c3.innerHTML="";var l=curLeagueObj();if(!l)return;var f=(filter||"").toLowerCase();
 l.events.forEach(function(e){
  var name=e.away?(e.home+" "+(T.vs||"vs")+" "+e.away):e.home;
  if(f&&name.toLowerCase().indexOf(f)<0)return;
  var r=el("div","ev");r.tabIndex=0;
  var hb='<span class="eb">'+(e.hb?'<img src="'+esc(e.hb)+'" alt="" onerror="this.remove()">':'')+'</span>';
  var ab='<span class="eb">'+(e.ab?'<img src="'+esc(e.ab)+'" alt="" onerror="this.remove()">':'')+'</span>';
  var st="";
  if(e.st==="live")st=' <span class="stx live">'+esc(T.live||"LIVE")+(e.sc?" "+esc(e.sc):"")+'</span>';
  else if(e.st==="final")st=' <span class="stx fin">'+esc(T.final||"Final")+(e.sc?" "+esc(e.sc):"")+'</span>';
  var chips="";
  if(e.ch&&e.ch.length){chips='<div class="ch">';e.ch.forEach(function(c){chips+='<span>'+esc(c)+'</span>'});chips+='</div>';}
  var mid='<div class="mid"><div class="mt">'+esc(name)+'</div><div class="dt">'+esc(dt(e))+st+'</div>'+chips+'</div>';
  r.innerHTML=e.away?(hb+mid+ab):('<span class="eb"></span>'+mid+'<span class="eb"></span>');
  c3.appendChild(r);
 });
}
s1.addEventListener("input",function(){var v=(this.value||"").toLowerCase();[].slice.call(c1.children).forEach(function(r){var n=r.querySelector(".nm");r.classList.toggle("hide",!!v&&n&&n.textContent.toLowerCase().indexOf(v)<0)})});
s2.addEventListener("input",function(){renderLeagues(this.value)});
s3.addEventListener("input",function(){renderEvents(this.value)});
function foc(){return [].slice.call(document.querySelectorAll(".row:not(.hide),.ev:not(.hide)"))}
function move(dir){var cur=document.activeElement,list=foc();if(!cur||list.indexOf(cur)<0){if(list[0])list[0].focus();return}
 var cr=cur.getBoundingClientRect(),cx=cr.left+cr.width/2,cy=cr.top+cr.height/2,best=null,bs=1e9;
 list.forEach(function(e){if(e===cur)return;var r=e.getBoundingClientRect(),x=r.left+r.width/2,y=r.top+r.height/2,dx=x-cx,dy=y-cy;
  var ok=(dir=="left"&&dx<-4)||(dir=="right"&&dx>4)||(dir=="up"&&dy<-4)||(dir=="down"&&dy>4);if(!ok)return;
  var ax=(dir=="left"||dir=="right")?Math.abs(dx):Math.abs(dy),cs=(dir=="left"||dir=="right")?Math.abs(dy):Math.abs(dx),s=ax+cs*2.2;
  if(s<bs){bs=s;best=e}});
 if(best){best.focus();if(best.scrollIntoView)best.scrollIntoView({block:"nearest",inline:"nearest"})}}
document.addEventListener("keydown",function(e){var a=document.activeElement;if(a&&a.tagName=="INPUT")return;var k=e.key,c=e.keyCode;
 if(k=="ArrowLeft"||c==37){e.preventDefault();move("left")}
 else if(k=="ArrowRight"||c==39){e.preventDefault();move("right")}
 else if(k=="ArrowUp"||c==38){e.preventDefault();move("up")}
 else if(k=="ArrowDown"||c==40){e.preventDefault();move("down")}});
renderSports();
selectSport(sports.length?sports[0].key:"all");
var f0=c1.querySelector(".row");if(f0){try{f0.focus({preventScroll:true})}catch(e){f0.focus()}}
})();
JS;

    echo '<!doctype html><html lang="' . $esc($lang) . '"><head><meta charset="utf-8">'
       . '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">'
       . '<title>' . $esc($catName('soccer')) . '</title><style>' . $css . '</style></head><body>'
       . '<div class="app">'
       . '<section class="pane" id="p1"><input class="search" id="s1" type="search"><div class="list" id="c1"></div></section>'
       . '<section class="pane" id="p2"><input class="search" id="s2" type="search"><div class="list" id="c2"></div></section>'
       . '<section class="pane" id="p3"><div class="detail" id="c3h"></div><input class="search" id="s3" type="search"><div class="list" id="c3"></div></section>'
       . '</div>'
       . '<script>window.__SPORTS__=' . $json . ';window.__T__=' . $tj . ';</script>'
       . '<script>' . $js . '</script></body></html>';
    exit;
}