<?php
/**
 * =====================================================================
 * Nadi Player — World Cup (JSON) module  ·  lib_worldcup.php
 * =====================================================================
 * A SELF-CONTAINED, MODULAR sports module that replaces the paid
 * api-football dependency with free, static openfootball JSON
 * (https://github.com/openfootball/worldcup.json).
 *
 *   • Source of truth  : `worldcup_matches` table (normalised rows).
 *   • Raw upload/sync   : also mirrored to api/sports/data/worldcup.json
 *                         so you keep an audit copy ("Both" storage).
 *   • Manual override   : a row can be `locked` so Auto-Sync never
 *                         overwrites your hand-typed live scores.
 *   • Auto-Sync         : cron pulls the openfootball raw URL and
 *                         upserts every NON-locked row.
 *   • Notifications     : reuses the existing FCM helpers
 *                         (api/routes/notifications.php → fcm_send_batch)
 *                         for match-day kickoff + goal alerts.
 *
 * IMPORTANT: This module touches NOTHING in the streaming engine, the
 * ad-reward system, or M3U/Xtream parsing. It only adds new tables and
 * new endpoints.
 *
 * Requires (already present in this backend): db(), send_json(),
 * json_error() — pulled in by the caller (fixtures.php / index.php).
 * =====================================================================
 */

if (!defined('WC_DEFAULT_SOURCE_URL')) {
    define('WC_DEFAULT_SOURCE_URL',
        'https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json');
}
if (!defined('WC_DATA_FILE')) {
    define('WC_DATA_FILE', __DIR__ . '/data/worldcup.json');
}

/* ──────────────────────────────────────────────────────────────────
 *  Schema (idempotent)
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_ensure_schema')) {
function wc_ensure_schema(): void {
    static $done = false;
    if ($done) return;
    $done = true;
    db()->exec(
        "CREATE TABLE IF NOT EXISTS `worldcup_matches` (
            `match_key`           VARCHAR(80)  NOT NULL PRIMARY KEY,
            `num`                 INT                   DEFAULT NULL,
            `round`               VARCHAR(80)  NOT NULL DEFAULT '',
            `group_name`          VARCHAR(40)           DEFAULT NULL,
            `match_date`          VARCHAR(20)  NOT NULL DEFAULT '',
            `match_time`          VARCHAR(40)           DEFAULT NULL,
            `kickoff_utc`         VARCHAR(40)           DEFAULT NULL,
            `team1`               VARCHAR(120) NOT NULL DEFAULT '',
            `team2`               VARCHAR(120) NOT NULL DEFAULT '',
            `code1`               VARCHAR(12)  NOT NULL DEFAULT '',
            `code2`               VARCHAR(12)  NOT NULL DEFAULT '',
            `score1`              INT                   DEFAULT NULL,
            `score2`              INT                   DEFAULT NULL,
            `pen1`                INT                   DEFAULT NULL,
            `pen2`                INT                   DEFAULT NULL,
            `status`              VARCHAR(16)  NOT NULL DEFAULT 'UPCOMING',
            `venue`               VARCHAR(160) NOT NULL DEFAULT '',
            `broadcaster`         VARCHAR(160)          DEFAULT NULL,
            `locked`              TINYINT(1)   NOT NULL DEFAULT 0,
            `notified_kickoff`    TINYINT(1)   NOT NULL DEFAULT 0,
            `last_notified_score` VARCHAR(16)           DEFAULT NULL,
            `sort_order`          INT          NOT NULL DEFAULT 0,
            `updated_at`          VARCHAR(40)  NOT NULL DEFAULT '',
            INDEX `ix_wc_date`   (`match_date`),
            INDEX `ix_wc_status` (`status`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
    );
}}

/* ──────────────────────────────────────────────────────────────────
 *  Settings helpers (reuse the shared `settings` global doc)
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_settings')) {
function wc_settings(): array {
    $row = db()->query("SELECT data FROM settings WHERE id = 'global'")->fetch();
    $s = $row ? json_decode($row['data'], true) : [];
    if (!is_array($s)) $s = [];
    return [
        'enabled'      => (bool)  ($s['worldcup_module_enabled'] ?? false),
        'autosync'     => (bool)  ($s['worldcup_autosync_enabled'] ?? false),
        'notify'       => (bool)  ($s['worldcup_notify_enabled'] ?? false),
        'source_type'  => in_array(($s['worldcup_source_type'] ?? 'openfootball'), ['openfootball','footballdata'], true)
                            ? $s['worldcup_source_type'] : 'openfootball',
        'fd_token'     => trim((string)($s['worldcup_fd_token'] ?? '')),
        'source_url'   => trim((string)($s['worldcup_source_url'] ?? '')) ?: WC_DEFAULT_SOURCE_URL,
        'last_sync'    => (string)($s['worldcup_last_sync'] ?? ''),
        'tpl_kickoff'  => (string)($s['worldcup_tpl_kickoff'] ?? 'Match starting now: {home} vs {away}!'),
        'tpl_goal'     => (string)($s['worldcup_tpl_goal']    ?? 'Goal! {home} {hs} - {as} {away}'),
        'remind_mins'  => max(0, min(180, (int)($s['worldcup_remind_mins'] ?? 10))),
    ];
}}

if (!function_exists('wc_settings_save')) {
function wc_settings_save(array $patch): void {
    $row = db()->query("SELECT data FROM settings WHERE id = 'global'")->fetch();
    $s = $row ? json_decode($row['data'], true) : [];
    if (!is_array($s)) $s = [];
    foreach ($patch as $k => $v) $s[$k] = $v;
    $stmt = db()->prepare(
        "INSERT INTO settings (id, data) VALUES ('global', ?)
         ON DUPLICATE KEY UPDATE data = VALUES(data)"
    );
    $stmt->execute([json_encode($s, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]);
}}

/* ──────────────────────────────────────────────────────────────────
 *  Team name → ISO-3166-1 alpha-2 (for flagcdn flags in the app)
 *  Derived from the supplied worldcup.teams.json; placeholders
 *  ("W101", "1E", "3A/B/C/D/F") resolve to "" (no flag).
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_team_code')) {
function wc_team_code(string $name): string {
    static $map = null;
    if ($map === null) {
        $map = [
            'mexico' => 'mx', 'south africa' => 'za', 'south korea' => 'kr',
            'korea republic' => 'kr', 'korea rep.' => 'kr', 'czech republic' => 'cz',
            'czechia' => 'cz', 'canada' => 'ca', 'bosnia & herzegovina' => 'ba',
            'bosnia and herzegovina' => 'ba', 'qatar' => 'qa', 'switzerland' => 'ch',
            'brazil' => 'br', 'morocco' => 'ma', 'haiti' => 'ht', 'scotland' => 'gb-sct',
            'usa' => 'us', 'united states' => 'us', 'paraguay' => 'py', 'australia' => 'au',
            'turkey' => 'tr', 'türkiye' => 'tr', 'germany' => 'de', 'curacao' => 'cw',
            'curaçao' => 'cw', 'ivory coast' => 'ci', "côte d'ivoire" => 'ci',
            'ecuador' => 'ec', 'netherlands' => 'nl', 'japan' => 'jp', 'sweden' => 'se',
            'tunisia' => 'tn', 'belgium' => 'be', 'egypt' => 'eg', 'iran' => 'ir',
            'new zealand' => 'nz', 'spain' => 'es', 'cape verde' => 'cv', 'cabo verde' => 'cv',
            'saudi arabia' => 'sa', 'uruguay' => 'uy', 'france' => 'fr', 'senegal' => 'sn',
            'iraq' => 'iq', 'norway' => 'no', 'argentina' => 'ar', 'algeria' => 'dz',
            'austria' => 'at', 'jordan' => 'jo', 'portugal' => 'pt', 'dr congo' => 'cd',
            'uzbekistan' => 'uz', 'colombia' => 'co', 'england' => 'gb-eng', 'croatia' => 'hr',
            'ghana' => 'gh', 'panama' => 'pa', 'wales' => 'gb-wls', 'serbia' => 'rs',
            'poland' => 'pl', 'denmark' => 'dk', 'italy' => 'it', 'nigeria' => 'ng',
            'cameroon' => 'cm', 'greece' => 'gr', 'ukraine' => 'ua', 'northern ireland' => 'gb-nir',
        ];
    }
    $k = strtolower(trim($name));
    return $map[$k] ?? '';
}}

/* ──────────────────────────────────────────────────────────────────
 *  "13:00 UTC-6" / "20:45 CET" + "2026-06-11"  →  ISO-8601 UTC
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_to_utc')) {
function wc_to_utc(string $date, ?string $time): ?string {
    $date = trim($date);
    if ($date === '') return null;
    $time = trim((string)$time);
    // Default midnight if no time component.
    $hh = '00'; $mm = '00'; $offset = '+00:00';

    if ($time !== '' && preg_match('/(\d{1,2}):(\d{2})\s*(.*)$/', $time, $m)) {
        $hh = str_pad($m[1], 2, '0', STR_PAD_LEFT);
        $mm = $m[2];
        $tz = strtoupper(trim($m[3]));
        if ($tz !== '') {
            if (preg_match('/UTC\s*([+-])\s*(\d{1,2})(?::?(\d{2}))?/', $tz, $o)) {
                $sign = $o[1];
                $oh   = str_pad($o[2], 2, '0', STR_PAD_LEFT);
                $om   = isset($o[3]) ? $o[3] : '00';
                $offset = $sign . $oh . ':' . $om;
            } else {
                // Common named abbreviations (hours from UTC).
                $named = [
                    'UTC' => 0, 'GMT' => 0, 'WET' => 0, 'BST' => 1, 'WEST' => 1,
                    'CET' => 1, 'CEST' => 2, 'EET' => 2, 'EEST' => 3, 'MSK' => 3,
                    'EST' => -5, 'EDT' => -4, 'CST' => -6, 'CDT' => -5,
                    'MST' => -7, 'MDT' => -6, 'PST' => -8, 'PDT' => -7, 'AST' => -4,
                ];
                if (array_key_exists($tz, $named)) {
                    $h = $named[$tz];
                    $offset = sprintf('%+03d:00', $h);
                }
            }
        }
    }

    try {
        $dt = new DateTime("{$date}T{$hh}:{$mm}:00{$offset}");
        $dt->setTimezone(new DateTimeZone('UTC'));
        return $dt->format('Y-m-d\TH:i:s\Z');
    } catch (Throwable $e) {
        return $date . 'T00:00:00Z';
    }
}}

/* Stable, deterministic key for an openfootball match (no id field). */
if (!function_exists('wc_match_key')) {
function wc_match_key(array $m): string {
    if (isset($m['num']) && (int)$m['num'] > 0) return 'n' . (int)$m['num'];
    $basis = ($m['date'] ?? '') . '|' . ($m['team1'] ?? '') . '|' . ($m['team2'] ?? '');
    return 'h' . substr(md5($basis), 0, 16);
}}

/* Auto status from a match payload (date/time + score presence). */
if (!function_exists('wc_auto_status')) {
function wc_auto_status(?string $kickoff_utc, $score1, $score2): string {
    if ($score1 !== null && $score2 !== null) return 'FT';
    if (!$kickoff_utc) return 'UPCOMING';
    $ko = strtotime($kickoff_utc);
    if ($ko === false) return 'UPCOMING';
    $now = time();
    if ($now < $ko) return 'UPCOMING';
    // Within ~2.5h of kickoff and no score yet → treat as LIVE (best-effort;
    // admin can lock + override for precise control).
    if ($now <= $ko + 9000) return 'LIVE';
    return 'UPCOMING';
}}

/* ──────────────────────────────────────────────────────────────────
 *  Import an openfootball worldcup.json payload into the DB.
 *  Respects `locked` rows (Auto-Sync never overwrites manual edits).
 *
 *  Returns: ['imported'=>int,'skipped_locked'=>int,'goal_events'=>[...]]
 *  where each goal_event = ['key','team1','team2','code1','code2',
 *                           'score1','score2','status'] for matches whose
 *  score changed during this import (used by the notifier).
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_import_openfootball')) {
function wc_import_openfootball(array $payload): array {
    wc_ensure_schema();
    $matches = $payload['matches'] ?? null;
    if (!is_array($matches)) {
        throw new RuntimeException('JSON has no "matches" array');
    }

    // Snapshot existing rows for lock check + change detection.
    $existing = [];
    foreach (db()->query(
        'SELECT match_key, locked, score1, score2, status, last_notified_score FROM worldcup_matches'
    )->fetchAll() as $r) {
        $existing[$r['match_key']] = $r;
    }

    $now = gmdate('c');
    $imported = 0; $skipped = 0; $order = 0; $goal_events = [];

    $up = db()->prepare(
        "INSERT INTO worldcup_matches
            (match_key, num, round, group_name, match_date, match_time, kickoff_utc,
             team1, team2, code1, code2, score1, score2, pen1, pen2, status, venue,
             sort_order, updated_at)
         VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
         ON DUPLICATE KEY UPDATE
            num=VALUES(num), round=VALUES(round), group_name=VALUES(group_name),
            match_date=VALUES(match_date), match_time=VALUES(match_time),
            kickoff_utc=VALUES(kickoff_utc), team1=VALUES(team1), team2=VALUES(team2),
            code1=VALUES(code1), code2=VALUES(code2), score1=VALUES(score1),
            score2=VALUES(score2), pen1=VALUES(pen1), pen2=VALUES(pen2),
            status=VALUES(status), venue=VALUES(venue), sort_order=VALUES(sort_order),
            updated_at=VALUES(updated_at)"
    );

    foreach ($matches as $m) {
        if (!is_array($m)) continue;
        $order++;
        $key = wc_match_key($m);

        // LOCK: skip rows the admin manually overrode.
        if (isset($existing[$key]) && (int)$existing[$key]['locked'] === 1) {
            $skipped++;
            continue;
        }

        $team1 = (string)($m['team1'] ?? 'TBD');
        $team2 = (string)($m['team2'] ?? 'TBD');
        $kickoff = wc_to_utc((string)($m['date'] ?? ''), $m['time'] ?? null);

        $score1 = null; $score2 = null; $pen1 = null; $pen2 = null;
        if (isset($m['score']) && is_array($m['score'])) {
            if (isset($m['score']['ft']) && is_array($m['score']['ft']) && count($m['score']['ft']) === 2) {
                $score1 = (int)$m['score']['ft'][0];
                $score2 = (int)$m['score']['ft'][1];
            }
            if (isset($m['score']['p']) && is_array($m['score']['p']) && count($m['score']['p']) === 2) {
                $pen1 = (int)$m['score']['p'][0];
                $pen2 = (int)$m['score']['p'][1];
            }
        }
        $status = wc_auto_status($kickoff, $score1, $score2);

        // Goal/score-change detection (only when a real score is present).
        if ($score1 !== null && $score2 !== null && isset($existing[$key])) {
            $old1 = $existing[$key]['score1'];
            $old2 = $existing[$key]['score2'];
            if ((string)$old1 !== (string)$score1 || (string)$old2 !== (string)$score2) {
                $goal_events[] = [
                    'key' => $key, 'team1' => $team1, 'team2' => $team2,
                    'code1' => wc_team_code($team1), 'code2' => wc_team_code($team2),
                    'score1' => $score1, 'score2' => $score2, 'status' => $status,
                ];
            }
        }

        $up->execute([
            $key,
            isset($m['num']) ? (int)$m['num'] : null,
            (string)($m['round'] ?? ''),
            isset($m['group']) ? (string)$m['group'] : null,
            (string)($m['date'] ?? ''),
            isset($m['time']) ? (string)$m['time'] : null,
            $kickoff,
            $team1, $team2,
            wc_team_code($team1), wc_team_code($team2),
            $score1, $score2, $pen1, $pen2,
            $status,
            (string)($m['ground'] ?? $m['city'] ?? ''),
            $order, $now,
        ]);
        $imported++;
    }

    return ['imported' => $imported, 'skipped_locked' => $skipped, 'goal_events' => $goal_events];
}}

/* Persist the raw JSON to the on-disk audit copy ("Both" storage). */
if (!function_exists('wc_write_raw_file')) {
function wc_write_raw_file(string $raw): bool {
    $dir = dirname(WC_DATA_FILE);
    if (!is_dir($dir)) @mkdir($dir, 0755, true);
    return @file_put_contents(WC_DATA_FILE, $raw) !== false;
}}

/* Fetch the openfootball raw JSON over HTTPS. */
if (!function_exists('wc_fetch_source')) {
function wc_fetch_source(string $url): string {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 20,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTPHEADER     => ['Accept: application/json', 'User-Agent: NadiPlayer-WC/1.0'],
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $body = curl_exec($ch);
    $errno = curl_errno($ch);
    $code  = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    // Stale-CA fallback common on shared hosts.
    if ($errno === 60) {
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        $body = curl_exec($ch);
        $errno = curl_errno($ch);
        $code  = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    }
    curl_close($ch);
    if ($body === false || $errno) throw new RuntimeException('Fetch failed: ' . curl_strerror($errno));
    if ($code >= 400) throw new RuntimeException("Source returned HTTP $code");
    return (string)$body;
}}

/* ──────────────────────────────────────────────────────────────────
 *  Sync now: fetch → import → mirror to file. Returns summary +
 *  goal_events (caller decides whether to notify).
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_sync_now')) {
function wc_sync_now(?string $url = null): array {
    $cfg = wc_settings();
    // Route to the configured data source.
    if ($cfg['source_type'] === 'footballdata') {
        return wc_sync_from_footballdata($cfg['fd_token'] ?: null);
    }
    $url = $url ?: $cfg['source_url'];
    $raw = wc_fetch_source($url);
    $payload = json_decode($raw, true);
    if (!is_array($payload)) throw new RuntimeException('Source is not valid JSON');
    $res = wc_import_openfootball($payload);
    wc_write_raw_file($raw);
    wc_settings_save(['worldcup_last_sync' => gmdate('c')]);
    $res['source'] = $url;
    return $res;
}}

/* ──────────────────────────────────────────────────────────────────
 *  Football-Data.org (v4) — competition WC. Token via X-Auth-Token.
 *  Free tier: 10 req/min. We cache each endpoint on disk (5 min) to
 *  stay well under the limit.
 * ────────────────────────────────────────────────────────────────── */
if (!defined('WC_FD_BASE'))  define('WC_FD_BASE', 'https://api.football-data.org/v4');
if (!defined('WC_FD_CACHE')) define('WC_FD_CACHE', __DIR__ . '/data');

if (!function_exists('wc_fd_get')) {
function wc_fd_get(string $path, string $token, int $ttl = 300): array {
    if ($token === '') throw new RuntimeException('Football-Data.org API token not set');
    $cacheFile = WC_FD_CACHE . '/fd_' . preg_replace('/[^a-z0-9]+/i', '_', $path) . '.json';
    if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
        $j = json_decode((string)@file_get_contents($cacheFile), true);
        if (is_array($j)) return $j;
    }
    $ch = curl_init(WC_FD_BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_HTTPHEADER     => ['X-Auth-Token: ' . $token, 'Accept: application/json'],
    ]);
    $body = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);
    if ($body === false) throw new RuntimeException("Football-Data fetch failed: $err");
    if ($code === 429)   throw new RuntimeException('Football-Data rate limit hit (429) — wait a minute');
    if ($code === 403)   throw new RuntimeException('Football-Data token rejected (403) — check your token/plan');
    if ($code >= 400)    throw new RuntimeException("Football-Data HTTP $code");
    $data = json_decode((string)$body, true);
    if (!is_array($data)) throw new RuntimeException('Football-Data returned non-JSON');
    if (!is_dir(WC_FD_CACHE)) @mkdir(WC_FD_CACHE, 0755, true);
    @file_put_contents($cacheFile, $body);
    return $data;
}}

/* FD status code → app status. */
if (!function_exists('wc_fd_status')) {
function wc_fd_status(string $s): string {
    $s = strtoupper(trim($s));
    if (in_array($s, ['IN_PLAY', 'PAUSED', 'SUSPENDED', 'LIVE'], true)) return 'LIVE';
    if (in_array($s, ['FINISHED', 'AWARDED'], true)) return 'FT';
    return 'UPCOMING'; // SCHEDULED, TIMED, POSTPONED, CANCELLED
}}

/* FD stage/group → app round + group label. */
if (!function_exists('wc_fd_round')) {
function wc_fd_round(?string $stage, ?string $group): array {
    $map = [
        'LAST_32' => 'Round of 32', 'LAST_16' => 'Round of 16',
        'QUARTER_FINALS' => 'Quarter-final', 'SEMI_FINALS' => 'Semi-final',
        'THIRD_PLACE' => 'Match for third place', 'FINAL' => 'Final',
        'GROUP_STAGE' => 'Group Stage', 'PRELIMINARY_ROUND' => 'Preliminary',
    ];
    $st = strtoupper((string)$stage);
    $round = $map[$st] ?? ($stage ? ucwords(strtolower(str_replace('_', ' ', $stage))) : '');
    $grp = null;
    if ($group && stripos($group, 'GROUP_') === 0) {
        $grp = 'Group ' . strtoupper(substr($group, 6));
    }
    return [$round, $grp];
}}

/* Pull matches + standings + teams from Football-Data.org → DB + settings. */
if (!function_exists('wc_sync_from_footballdata')) {
function wc_sync_from_footballdata(?string $token = null): array {
    wc_ensure_schema();
    $cfg = wc_settings();
    $token = $token ?: $cfg['fd_token'];
    if (!$token) throw new RuntimeException('Football-Data.org API token not set');

    // 1) Matches → worldcup_matches (lock-aware), with goal detection.
    $md = wc_fd_get('/competitions/WC/matches', $token, 60);
    $rows = is_array($md['matches'] ?? null) ? $md['matches'] : [];

    $existing = [];
    foreach (db()->query('SELECT match_key, locked, score1, score2 FROM worldcup_matches')->fetchAll() as $r) {
        $existing[$r['match_key']] = $r;
    }
    $now = gmdate('c'); $imported = 0; $skipped = 0; $order = 0; $goal_events = [];
    $up = db()->prepare(
        "INSERT INTO worldcup_matches
            (match_key, num, round, group_name, match_date, match_time, kickoff_utc,
             team1, team2, code1, code2, score1, score2, status, venue, sort_order, updated_at)
         VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
         ON DUPLICATE KEY UPDATE
            round=VALUES(round), group_name=VALUES(group_name), match_date=VALUES(match_date),
            kickoff_utc=VALUES(kickoff_utc), team1=VALUES(team1), team2=VALUES(team2),
            code1=VALUES(code1), code2=VALUES(code2), score1=VALUES(score1), score2=VALUES(score2),
            status=VALUES(status), sort_order=VALUES(sort_order), updated_at=VALUES(updated_at)"
    );
    foreach ($rows as $m) {
        if (!is_array($m)) continue;
        $order++;
        $key = 'fd' . ($m['id'] ?? $order);
        if (isset($existing[$key]) && (int)$existing[$key]['locked'] === 1) { $skipped++; continue; }

        [$round, $grp] = wc_fd_round($m['stage'] ?? null, $m['group'] ?? null);
        $utc = (string)($m['utcDate'] ?? '');
        $t1 = (string)($m['homeTeam']['name'] ?? 'TBD');
        $t2 = (string)($m['awayTeam']['name'] ?? 'TBD');
        $s1 = $m['score']['fullTime']['home'] ?? null;
        $s2 = $m['score']['fullTime']['away'] ?? null;
        $s1 = ($s1 === null) ? null : (int)$s1;
        $s2 = ($s2 === null) ? null : (int)$s2;
        $status = wc_fd_status((string)($m['status'] ?? ''));

        if ($s1 !== null && $s2 !== null && isset($existing[$key])) {
            if ((string)$existing[$key]['score1'] !== (string)$s1 || (string)$existing[$key]['score2'] !== (string)$s2) {
                $goal_events[] = ['key'=>$key,'team1'=>$t1,'team2'=>$t2,'score1'=>$s1,'score2'=>$s2,'status'=>$status];
            }
        }
        $up->execute([
            $key, null, $round, $grp, substr($utc, 0, 10), null, $utc ?: null,
            $t1, $t2, wc_team_code($t1), wc_team_code($t2),
            $s1, $s2, $status, '', $order, $now,
        ]);
        $imported++;
    }

    // 2) Standings → settings JSON (for the Groups table in the app).
    try {
        $sd = wc_fd_get('/competitions/WC/standings', $token, 120);
        $groups = [];
        foreach (($sd['standings'] ?? []) as $st) {
            if (($st['type'] ?? 'TOTAL') !== 'TOTAL') continue;
            $table = [];
            foreach (($st['table'] ?? []) as $r) {
                $name = (string)($r['team']['name'] ?? 'TBD');
                $table[] = [
                    'position' => (int)($r['position'] ?? 0),
                    'name'     => $name,
                    'code'     => wc_team_code($name),
                    'crest'    => (string)($r['team']['crest'] ?? ''),
                    'played'   => (int)($r['playedGames'] ?? 0),
                    'won'      => (int)($r['won'] ?? 0),
                    'draw'     => (int)($r['draw'] ?? 0),
                    'lost'     => (int)($r['lost'] ?? 0),
                    'gf'       => (int)($r['goalsFor'] ?? 0),
                    'ga'       => (int)($r['goalsAgainst'] ?? 0),
                    'gd'       => (int)($r['goalDifference'] ?? 0),
                    'points'   => (int)($r['points'] ?? 0),
                    'form'     => $r['form'] ?? null,
                ];
            }
            $groups[] = ['group' => (string)($st['group'] ?? ''), 'table' => $table];
        }
        wc_settings_save(['worldcup_standings' => json_encode($groups, JSON_UNESCAPED_UNICODE)]);
    } catch (Throwable $e) {
        // standings optional — don't fail the whole sync
    }

    wc_settings_save(['worldcup_last_sync' => gmdate('c')]);
    return ['imported'=>$imported, 'skipped_locked'=>$skipped, 'goal_events'=>$goal_events, 'source'=>'football-data.org'];
}}

/* Stored standings (group tables) for the app payload. */
if (!function_exists('wc_standings')) {
function wc_standings(): array {
    $row = db()->query("SELECT data FROM settings WHERE id = 'global'")->fetch();
    $s = $row ? json_decode($row['data'], true) : [];
    $g = is_array($s) ? json_decode((string)($s['worldcup_standings'] ?? '[]'), true) : [];
    return is_array($g) ? $g : [];
}}

/* ──────────────────────────────────────────────────────────────────
 *  Notification helpers (reuse fcm_send_batch from notifications.php)
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_render_tpl')) {
function wc_render_tpl(string $tpl, array $m): string {
    return strtr($tpl, [
        '{home}' => (string)($m['team1'] ?? ''),
        '{away}' => (string)($m['team2'] ?? ''),
        '{hs}'   => (string)($m['score1'] ?? '0'),
        '{as}'   => (string)($m['score2'] ?? '0'),
    ]);
}}

/* Send one match alert ('kickoff'|'goal') to all active devices.
 * Safe no-op (returns ['ok'=>false]) when FCM isn't available/configured. */
if (!function_exists('wc_notify_match')) {
function wc_notify_match(array $m, string $kind): array {
    if (!function_exists('fcm_send_batch') || !function_exists('fcm_service_account_path')) {
        return ['ok' => false, 'reason' => 'FCM module not loaded'];
    }
    if (!is_file(fcm_service_account_path())) {
        return ['ok' => false, 'reason' => 'Firebase service-account JSON missing'];
    }
    $cfg = wc_settings();
    $title = ($kind === 'goal') ? 'Goal!' : 'Kick-off';
    $body  = wc_render_tpl($kind === 'goal' ? $cfg['tpl_goal'] : $cfg['tpl_kickoff'], $m);

    $rows = db()->query('SELECT device_id, token FROM fcm_tokens WHERE is_active = 1')->fetchAll();
    $opts = [
        'title' => $title,
        'body'  => $body,
        'route' => '/world-cup',
        'data'  => ['kind' => 'worldcup_' . $kind, 'match_key' => (string)($m['key'] ?? $m['match_key'] ?? '')],
    ];
    $res = fcm_send_batch($rows, $opts, 'worldcup');
    return ['ok' => ($res['sent'] ?? 0) > 0, 'result' => $res];
}}

/* ──────────────────────────────────────────────────────────────────
 *  Cron tick — called from routes/cron.php on every cron run.
 *  • Pulls fresh data when Auto-Sync is ON.
 *  • Fires goal alerts for detected score changes.
 *  • Fires kickoff reminders for matches starting within `remind_mins`.
 *  All notifications are gated behind the `notify` setting.
 * ────────────────────────────────────────────────────────────────── */
if (!function_exists('wc_cron_tick')) {
function wc_cron_tick(): array {
    wc_ensure_schema();
    $cfg = wc_settings();
    $out = ['autosync' => $cfg['autosync'], 'synced' => 0, 'skipped_locked' => 0,
            'goal_alerts' => 0, 'kickoff_alerts' => 0, 'errors' => []];

    if (!$cfg['enabled']) { $out['disabled'] = true; return $out; }

    // 1) Auto-sync (pull from openfootball) when enabled.
    if ($cfg['autosync']) {
        try {
            $res = wc_sync_now($cfg['source_url']);
            $out['synced'] = $res['imported'];
            $out['skipped_locked'] = $res['skipped_locked'];
            // 2) Goal alerts for score changes (deduped via last_notified_score).
            if ($cfg['notify'] && !empty($res['goal_events'])) {
                foreach ($res['goal_events'] as $ev) {
                    $sig = $ev['score1'] . '-' . $ev['score2'];
                    $chk = db()->prepare('SELECT last_notified_score FROM worldcup_matches WHERE match_key = ?');
                    $chk->execute([$ev['key']]);
                    $last = (string)($chk->fetchColumn() ?: '');
                    if ($last === $sig) continue; // already alerted this score
                    $r = wc_notify_match($ev, 'goal');
                    if (!empty($r['ok'])) $out['goal_alerts']++;
                    $upd = db()->prepare('UPDATE worldcup_matches SET last_notified_score = ? WHERE match_key = ?');
                    $upd->execute([$sig, $ev['key']]);
                }
            }
        } catch (Throwable $e) {
            $out['errors'][] = 'sync: ' . $e->getMessage();
        }
    }

    // 3) Kickoff reminders — matches starting within the reminder window.
    if ($cfg['notify']) {
        try {
            $now = time();
            $window = $now + ($cfg['remind_mins'] * 60);
            $rows = db()->query(
                "SELECT match_key, team1, team2, kickoff_utc
                   FROM worldcup_matches
                  WHERE status <> 'FT' AND notified_kickoff = 0 AND kickoff_utc IS NOT NULL"
            )->fetchAll();
            foreach ($rows as $r) {
                $ko = strtotime((string)$r['kickoff_utc']);
                if ($ko === false) continue;
                // Fire when kickoff is within [now-15min, window].
                if ($ko <= $window && $ko >= $now - 900) {
                    $rr = wc_notify_match($r, 'kickoff');
                    $upd = db()->prepare('UPDATE worldcup_matches SET notified_kickoff = 1 WHERE match_key = ?');
                    $upd->execute([$r['match_key']]);
                    if (!empty($rr['ok'])) $out['kickoff_alerts']++;
                }
            }
        } catch (Throwable $e) {
            $out['errors'][] = 'kickoff: ' . $e->getMessage();
        }
    }

    return $out;
}}

/* ──────────────────────────────────────────────────────────────────
 *  24h-cached GitHub passthrough  →  { configured, matches }
 *  Used by fixtures_wc2026.php as a ZERO-CONFIG bootstrap: if the admin
 *  hasn't synced/uploaded into the DB yet, this fetches the openfootball
 *  raw JSON directly, caches it on disk for 24h, transforms it on the
 *  fly, and serves it. Once the admin uses Sync/Upload, the DB takes
 *  over (so manual edits + locks always win).
 * ────────────────────────────────────────────────────────────────── */
if (!defined('WC_CACHE_FILE')) {
    define('WC_CACHE_FILE', __DIR__ . '/data/wc2026_cache.json');
}
if (!function_exists('wc_github_cached_payload')) {
function wc_github_cached_payload(int $ttl = 86400): array {
    $cfg = wc_settings();
    $url = $cfg['source_url'];
    $raw = null;

    // Serve from disk cache when it's fresh (< $ttl seconds old).
    if (is_file(WC_CACHE_FILE) && (time() - filemtime(WC_CACHE_FILE)) < $ttl) {
        $raw = @file_get_contents(WC_CACHE_FILE);
    }
    // Otherwise refresh from GitHub (fall back to a stale cache on error).
    if ($raw === null || $raw === false || $raw === '') {
        try {
            $raw = wc_fetch_source($url);
            $dir = dirname(WC_CACHE_FILE);
            if (!is_dir($dir)) @mkdir($dir, 0755, true);
            @file_put_contents(WC_CACHE_FILE, $raw);
        } catch (Throwable $e) {
            if (is_file(WC_CACHE_FILE)) $raw = @file_get_contents(WC_CACHE_FILE); // stale is better than nothing
            if ($raw === null || $raw === false) {
                return ['configured' => false, 'matches' => [], 'error' => $e->getMessage()];
            }
        }
    }

    $payload = json_decode((string)$raw, true);
    $list = (is_array($payload) && isset($payload['matches']) && is_array($payload['matches']))
        ? $payload['matches'] : [];

    $matches = [];
    foreach ($list as $i => $m) {
        if (!is_array($m)) continue;
        $kickoff = wc_to_utc((string)($m['date'] ?? ''), $m['time'] ?? null);
        $s1 = null; $s2 = null;
        if (isset($m['score']['ft']) && is_array($m['score']['ft']) && count($m['score']['ft']) === 2) {
            $s1 = (int)$m['score']['ft'][0];
            $s2 = (int)$m['score']['ft'][1];
        }
        $t1 = (string)($m['team1'] ?? 'TBD');
        $t2 = (string)($m['team2'] ?? 'TBD');
        $matches[] = [
            'id'        => wc_match_key($m),
            'home'      => ['name' => $t1, 'code' => wc_team_code($t1)],
            'away'      => ['name' => $t2, 'code' => wc_team_code($t2)],
            'homeScore' => $s1,
            'awayScore' => $s2,
            'status'    => wc_auto_status($kickoff, $s1, $s2),
            'kickoff'   => $kickoff,
            'group'     => $m['group'] ?? null,
            'round'     => (string)($m['round'] ?? ''),
            'venue'     => (string)($m['ground'] ?? $m['city'] ?? ''),
        ];
    }
    return [
        'configured' => true,
        'matches'    => $matches,
        'meta'       => ['count' => count($matches), 'source' => 'github-cache', 'cached_24h' => true],
    ];
}}

/* App payload with GitHub-cache fallback (used by fixtures_wc2026.php). */
if (!function_exists('wc_payload_with_fallback')) {
function wc_payload_with_fallback(): array {
    $db = wc_app_payload();           // DB first (honours manual edits + locks)
    if (!empty($db['configured']) && !empty($db['matches'])) {
        return $db;
    }
    // DB empty (admin hasn't synced yet) → zero-config 24h GitHub cache.
    return wc_github_cached_payload();
}}
if (!function_exists('wc_app_payload')) {
function wc_app_payload(): array {
    wc_ensure_schema();
    $cfg = wc_settings();
    if (!$cfg['enabled']) {
        return ['configured' => false, 'matches' => []];
    }
    $rows = db()->query(
        'SELECT * FROM worldcup_matches ORDER BY sort_order ASC, kickoff_utc ASC'
    )->fetchAll();

    $matches = [];
    foreach ($rows as $r) {
        $matches[] = [
            'id'        => (string)$r['match_key'],
            'home'      => ['name' => $r['team1'], 'code' => $r['code1']],
            'away'      => ['name' => $r['team2'], 'code' => $r['code2']],
            'homeScore' => $r['score1'] === null ? null : (int)$r['score1'],
            'awayScore' => $r['score2'] === null ? null : (int)$r['score2'],
            'homePenalty' => $r['pen1'] === null ? null : (int)$r['pen1'],
            'awayPenalty' => $r['pen2'] === null ? null : (int)$r['pen2'],
            'status'    => (string)$r['status'],
            'kickoff'   => $r['kickoff_utc'],
            'group'     => $r['group_name'],
            'round'     => $r['round'],
            'venue'     => $r['venue'],
            'broadcaster' => $r['broadcaster'],
        ];
    }
    return [
        'configured' => true,
        'matches'    => $matches,
        'standings'  => wc_standings(),
        'meta'       => ['count' => count($matches), 'last_sync' => $cfg['last_sync'], 'source' => $cfg['source_type']],
    ];
}}
