<?php
/**
 * match.php  —  /api2/match.json এর মতো আউটপুট দেয়
 * URL: https://api.eplayhd.com/api2/match.php   (বা .htaccess দিয়ে match.json বানান)
 *
 * ৫ মিনিট disk cache — নতুন ম্যাচ/সার্ভার এলে অটো আপডেট।
 * প্রতি রিফ্রেশে stream link auto-check হয়।
 *   - কাজ না করলে JSON এ যোগ হয় না
 *   - HLS হলে master playlist-এর ভিতরের child playlist / segment-ও চেক হয়
 *   - upstream headers থাকলে check request-এ সেগুলো ব্যবহার হয়
 */

// ==== কনফিগ ====
const UPSTREAM_URL = 'https://api2.eplayhd.fun/api.php?action=matches';
const CACHE_TTL    = 300;              // 5 মিনিট — এর পর background refresh
const CACHE_MAX_AGE = 3600;            // 1 ঘন্টা — stale সার্ভ করার maximum সময়
const CACHE_FILE   = __DIR__ . '/cache/match.json';
const HEALTH_FILE  = __DIR__ . '/cache/health.json';
const LOCK_FILE    = __DIR__ . '/cache/refresh.lock';
const CHECK_TIMEOUT = 6;               // per-link timeout (sec)
const CHECK_CONCURRENCY = 30;          // curl_multi parallelism
const CHECK_BODY_LIMIT = 65536;         // manifest/segment probe max bytes

// playerUrl অটো — যেই ডোমেইন/ফোল্ডারে match.php আছে, সেখানকার player.php ব্যবহার হবে
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
$dir    = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/');
define('PLAYER_BASE', $scheme . '://' . $host . $dir . '/player.php');
const MATCH_HOST   = 'http://www.fawanews.sc/';

date_default_timezone_set('Asia/Dhaka');
if (!is_dir(dirname(CACHE_FILE))) @mkdir(dirname(CACHE_FILE), 0755, true);

header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: public, max-age=60');

// ==== Stale-While-Revalidate ====
// fresh হলে সরাসরি সার্ভ
$cacheAge = file_exists(CACHE_FILE) ? (time() - filemtime(CACHE_FILE)) : PHP_INT_MAX;
if ($cacheAge < CACHE_TTL) {
    readfile(CACHE_FILE);
    exit;
}

// stale কিন্তু ব্যবহারযোগ্য — সার্ভ করে দাও, তারপর background-এ refresh
if (file_exists(CACHE_FILE) && $cacheAge < CACHE_MAX_AGE) {
    header('X-Cache: STALE');
    readfile(CACHE_FILE);
    // response client-কে পাঠিয়ে দাও, তারপরই ভারী কাজ
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    } else {
        // fallback: buffer flush
        while (ob_get_level()) ob_end_flush();
        flush();
    }
    // একটাই refresh worker চলবে
    if (!acquire_refresh_lock()) exit;
    try { refresh_cache(); } finally { release_refresh_lock(); }
    exit;
}

// cache নেই / MAX_AGE-ও পার — একটাই worker refresh করুক, বাকিরা অপেক্ষা না করে খালি response
if (!acquire_refresh_lock()) {
    // অন্য কেউ ইতিমধ্যে refresh করছে — খালি response দাও, পরের request-এ পাবে
    echo json_encode(['updatedAt' => gmdate('c'), 'count' => 0, 'matches' => [], 'status' => 'refreshing']);
    exit;
}
try {
    refresh_cache();
} finally {
    release_refresh_lock();
}
exit;

// =====================================================
function acquire_refresh_lock(): bool {
    $fp = @fopen(LOCK_FILE, 'c');
    if (!$fp) return true; // lock না দিতে পারলেও চালিয়ে যাও
    if (!flock($fp, LOCK_EX | LOCK_NB)) { fclose($fp); return false; }
    // stale lock (>2 মিনিট) কে override
    $GLOBALS['__lock_fp'] = $fp;
    return true;
}
function release_refresh_lock(): void {
    if (!empty($GLOBALS['__lock_fp'])) {
        flock($GLOBALS['__lock_fp'], LOCK_UN);
        fclose($GLOBALS['__lock_fp']);
        @unlink(LOCK_FILE);
    }
}

function refresh_cache(): void {
// ==== upstream fetch ====
$ch = curl_init(UPSTREAM_URL);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_USERAGENT      => 'ePlayHD-Aggregator/1.0',
    CURLOPT_HTTPHEADER     => ['Accept: application/json'],
]);
$resp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($resp === false || $http !== 200) {
    if (file_exists(CACHE_FILE)) { readfile(CACHE_FILE); exit; }
    echo json_encode(['updatedAt' => gmdate('c'), 'count' => 0, 'matches' => [], 'error' => 'upstream ' . $http]);
    exit;
}

$data = json_decode($resp, true);
if (!is_array($data) || empty($data['matches'])) return;

// ==== flatten ====
$all = [];
foreach ($data['matches'] as $m) {
    $slug     = $m['slug'] ?? ($m['id'] ?? '');
    $home     = $m['homeTeam'] ?? '';
    $away     = $m['awayTeam'] ?? '';
    $vsName   = trim($home && $away ? "$home vs $away" : ($m['title'] ?? ''));
    $league   = $m['competition'] ?? ($m['title'] ?? '');
    $image    = $m['homeLogo'] ?? '';
    $matchUrl = !empty($m['href']) ? $m['href'] : (MATCH_HOST . $slug . '.html');
    $matchId  = $m['id'] ?? $slug;

    $t = $m['time'] ?? 0;
    $time = (!$t || $t === 0) ? 'TBA' : date('Y-m-d H:i', (int)$t);

    $servers = $m['servers'] ?? [];
    if (!is_array($servers)) continue;

    $i = 0;
    foreach ($servers as $s) {
        $suffix = $i === 0 ? '' : '_' . $i;
        $all[] = [
            'id'         => $slug . $suffix . '.html',
            'name'       => $vsName . ' --- S' . ($i + 1),
            'league'     => $league,
            'time'       => $time,
            'image'      => $image,
            'matchUrl'   => $matchUrl,
            'playerUrl'  => PLAYER_BASE . '?m=' . urlencode($matchId) . '&s=' . urlencode($s['id'] ?? ''),
            'serverName' => $s['name'] ?? '',
            'streamUrl'  => $s['url']  ?? ($s['externalUrl'] ?? ''),
            'headers'    => $s['headers'] ?? [],
        ];
        $i++;
    }
}

// ==== parallel stream check ====
$results = check_entries_parallel($all);

// ==== filter: strictly working links only ====
$out = [];
foreach ($all as $idx => $entry) {
    if (!empty($results[$idx])) $out[] = $entry;
}

$payload = [
    'updatedAt' => gmdate('c'),
    'count'     => count($out),
    'matches'   => $out,
];

$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$tmp = CACHE_FILE . '.tmp';
if (@file_put_contents($tmp, $json, LOCK_EX) !== false) {
    @rename($tmp, CACHE_FILE);
}
} // end refresh_cache




// =====================================================
function check_entries_parallel(array $entries): array {
    $results = array_fill(0, count($entries), false);
    $indices = [];
    foreach ($entries as $i => $entry) {
        $u = normalize_stream_url($entry['streamUrl'] ?? '');
        if (is_string($u) && $u !== '' && preg_match('~^https?://~i', $u)) {
            $indices[] = $i;
        }
    }
    if (!$indices) return $results;

    $chunks = array_chunk($indices, CHECK_CONCURRENCY);
    foreach ($chunks as $chunk) {
        $mh = curl_multi_init();
        $handles = [];
        $bodies = [];
        foreach ($chunk as $i) {
            $url = normalize_stream_url($entries[$i]['streamUrl'] ?? '');
            $headers = $entries[$i]['headers'] ?? [];
            $bodies[$i] = '';
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_NOBODY         => false,
                CURLOPT_TIMEOUT        => CHECK_TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => 4,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS      => 3,
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_SSL_VERIFYHOST => 0,
                CURLOPT_USERAGENT      => header_value($headers, 'user-agent') ?: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36',
                CURLOPT_HTTPHEADER     => build_curl_headers($headers),
                CURLOPT_WRITEFUNCTION  => function($ch, $data) use (&$bodies, $i) {
                    $left = CHECK_BODY_LIMIT - strlen($bodies[$i]);
                    if ($left > 0) {
                        $bodies[$i] .= substr($data, 0, $left);
                    }
                    return strlen($data);
                },
            ]);
            curl_multi_add_handle($mh, $ch);
            $handles[$i] = $ch;
        }
        do {
            $status = curl_multi_exec($mh, $running);
            if ($running) curl_multi_select($mh, 1);
        } while ($running && $status === CURLM_OK);

        foreach ($handles as $i => $ch) {
            $err = (int)curl_errno($ch);
            $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $ctype = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
            $effective = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
            $results[$i] = validate_stream_response(
                normalize_stream_url($entries[$i]['streamUrl'] ?? ''),
                $entries[$i]['headers'] ?? [],
                $entries[$i]['serverName'] ?? '',
                $err,
                $code,
                $bodies[$i] ?? '',
                $ctype,
                $effective
            );
            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
        }
        curl_multi_close($mh);
    }
    return $results;
}

function normalize_stream_url($url): string {
    $url = trim((string)$url);
    if ($url === '') return '';
    // player.php pipe(|) options বাদ দিয়ে আসল playback URL চালায়
    if (strpos($url, '|') !== false) {
        $url = trim(explode('|', $url, 2)[0]);
    }
    return $url;
}

function header_value($headers, string $name): string {
    if (!is_array($headers)) return '';
    foreach ($headers as $k => $v) {
        if (strtolower((string)$k) === strtolower($name)) return trim((string)$v);
    }
    return '';
}

function build_curl_headers($headers): array {
    $out = ['Accept: */*'];
    if (!is_array($headers)) return $out;
    foreach ($headers as $k => $v) {
        $key = trim((string)$k);
        $val = trim((string)$v);
        if ($key === '' || $val === '') continue;
        if (strtolower($key) === 'user-agent') continue; // CURLOPT_USERAGENT দিয়ে সেট করা হয়
        $out[] = $key . ': ' . $val;
    }
    return $out;
}

function validate_stream_response(string $url, array $headers, string $serverName, int $curlErr, int $code, string $body, string $ctype, string $effectiveUrl = ''): bool {
    if ($url === '') return false;

    // Toffee BDIX direct CDN অনেক non-BD server/cPanel IP-তে 403 দেয়, কিন্তু BDIX user/player থেকে চলে।
    // অনেক hosting-এ DNS resolve-ও হয় না; তাই direct signed toffeelive URL হলে token valid থাকলে রাখা হবে।
    // wrapper/proxy playlist (যেমন cdn-tt.pages.dev) এই exception পাবে না।
    if (is_valid_toffee_direct_url($url)) return true;

    // এই নামের source HTTP 200 দিলেও অনেক সময় child stream/player-এ চলে না; তাই বাদ।
    if (preg_match('~^TSN\s+SPORTS\s+HD$~i', trim($serverName))) return false;

    if ($curlErr !== 0) return false;
    if ($code < 200 || $code >= 400) return false;

    $baseUrl = $effectiveUrl !== '' ? $effectiveUrl : $url;
    $lowerUrl = strtolower(parse_url($baseUrl, PHP_URL_PATH) ?: $baseUrl);
    $head = substr(ltrim($body), 0, 512);
    $lowerHead = strtolower($head);

    if (strpos($lowerHead, '<html') !== false || strpos($lowerHead, 'access denied') !== false || strpos($lowerHead, 'not found') !== false) {
        return false;
    }

    if (strpos($lowerUrl, '.m3u8') !== false || stripos($body, '#EXTM3U') !== false || stripos($ctype, 'mpegurl') !== false) {
        return validate_hls_playlist($baseUrl, $headers, $body, 0);
    }

    if (strpos($lowerUrl, '.mpd') !== false || stripos($body, '<MPD') !== false || stripos($ctype, 'dash+xml') !== false) {
        return validate_dash_manifest($body);
    }

    return is_probably_media_response($baseUrl, $ctype, $body) || $code === 206;
}

function validate_hls_playlist(string $playlistUrl, array $headers, string $body, int $depth): bool {
    if (stripos($body, '#EXTM3U') === false) return false;
    if ($depth > 2) return true;

    $lines = preg_split('/\r\n|\r|\n/', $body);
    $uris = [];
    $hasMediaTags = false;
    $invalidUriLine = false;

    foreach ($lines as $line) {
        $line = trim($line);
        if ($line === '') continue;
        if ($line[0] === '#') {
            if (stripos($line, '#EXTINF') === 0 || stripos($line, '#EXT-X-TARGETDURATION') === 0) {
                $hasMediaTags = true;
            }
            continue;
        }

        // JSON/HTML/error line থাকলে HLS parser-এ ভাঙে; শুধু valid URI line গ্রহণ করা হবে
        if ($line[0] === '{' || $line[0] === '}' || stripos($line, 'playbakcs_url') !== false || preg_match('~[<>"\\s]~', $line)) {
            $invalidUriLine = true;
            continue;
        }
        $uris[] = resolve_url($playlistUrl, $line);
    }

    if ($invalidUriLine || empty($uris)) return false;

    $limit = min(count($uris), 3);
    for ($i = 0; $i < $limit; $i++) {
        $probe = probe_url($uris[$i], $headers);
        if (!$probe['ok']) continue;

        $childBody = $probe['body'];
        $childType = $probe['contentType'];
        $childUrl = $probe['effectiveUrl'] ?: $uris[$i];

        if (stripos($childBody, '#EXTM3U') !== false || stripos($childType, 'mpegurl') !== false || stripos(parse_url($childUrl, PHP_URL_PATH) ?: '', '.m3u8') !== false) {
            if (validate_hls_playlist($childUrl, $headers, $childBody, $depth + 1)) return true;
        } else if (is_probably_media_response($childUrl, $childType, $childBody)) {
            return true; // segment/media bytes OK
        }
    }

    // media playlist হলে অন্তত একটি segment OK লাগবে; master playlist হলে child playlist OK লাগবে
    return false;
}

function probe_url(string $url, array $headers): array {
    $body = '';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_NOBODY         => false,
        CURLOPT_TIMEOUT        => CHECK_TIMEOUT,
        CURLOPT_CONNECTTIMEOUT => 4,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 3,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => 0,
        CURLOPT_USERAGENT      => header_value($headers, 'user-agent') ?: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36',
        CURLOPT_HTTPHEADER     => build_curl_headers($headers),
        CURLOPT_WRITEFUNCTION  => function($ch, $data) use (&$body) {
            $left = CHECK_BODY_LIMIT - strlen($body);
            if ($left > 0) $body .= substr($data, 0, $left);
            return strlen($data);
        },
    ]);
    curl_exec($ch);
    $err = (int)curl_errno($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $ctype = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    $effective = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
    curl_close($ch);

    $head = strtolower(substr(ltrim($body), 0, 512));
    $badText = strpos($head, '<html') !== false || strpos($head, 'access denied') !== false || strpos($head, 'not found') !== false || strpos($head, 'page not found') !== false || strpos($head, 'forbidden') !== false || strpos($head, 'not available') !== false || strpos($head, 'expired') !== false;
    return [
        'ok' => ($err === 0 && $code >= 200 && $code < 400 && !$badText),
        'body' => $body,
        'contentType' => $ctype,
        'effectiveUrl' => $effective,
    ];
}

function validate_dash_manifest(string $body): bool {
    if (stripos($body, '<MPD') === false) return false;

    // DRM/cenc DASH manifest player-এ license ছাড়া চলে না, কিন্তু HTTP 200 আসে — এগুলো JSON থেকে বাদ।
    if (stripos($body, '<ContentProtection') !== false || stripos($body, 'widevine') !== false || stripos($body, 'playready') !== false || stripos($body, 'cenc:') !== false || stripos($body, 'cmaf_cenc') !== false) {
        return false;
    }

    return stripos($body, '<SegmentTemplate') !== false || stripos($body, '<SegmentList') !== false || stripos($body, '<BaseURL') !== false;
}

function is_probably_media_response(string $url, string $ctype, string $body): bool {
    $path = strtolower(parse_url($url, PHP_URL_PATH) ?: $url);
    $type = strtolower($ctype);
    $head = strtolower(substr(ltrim($body), 0, 512));

    if ($body === '') return false;
    if (strpos($head, '<html') !== false || strpos($head, '{') === 0 || strpos($head, 'not found') !== false || strpos($head, 'page not found') !== false || strpos($head, 'access denied') !== false || strpos($head, 'forbidden') !== false || strpos($head, 'expired') !== false) {
        return false;
    }

    if (preg_match('~\.(ts|m4s|m4v|mp4|aac|mp3|webm|cmfv|cmfa)(\?|$)~i', $path)) return true;
    if (strpos($type, 'video/') !== false || strpos($type, 'audio/') !== false || strpos($type, 'octet-stream') !== false) return true;

    // MPEG-TS packets usually start with sync byte 0x47; fragmented MP4 starts with an ftyp/styp/moof box near the top.
    if (isset($body[0]) && ord($body[0]) === 0x47) return true;
    if (strpos(substr($body, 0, 64), 'ftyp') !== false || strpos(substr($body, 0, 64), 'styp') !== false || strpos(substr($body, 0, 64), 'moof') !== false) return true;

    return false;
}

function is_valid_toffee_direct_url(string $url): bool {
    $p = parse_url($url);
    $host = strtolower($p['host'] ?? '');
    $path = strtolower($p['path'] ?? '');
    $query = $p['query'] ?? '';

    if (!preg_match('~(^|\.)toffeelive\.com$~', $host)) return false;
    if (strpos($path, '.m3u8') === false) return false;
    return strpos($query, 'hdntl=') !== false || strpos($query, 'Expires=') !== false || strpos($query, 'Signature=') !== false;
}

function resolve_url(string $base, string $rel): string {
    if (preg_match('~^https?://~i', $rel)) return $rel;
    $p = parse_url($base);
    if (!$p || empty($p['scheme']) || empty($p['host'])) return $rel;

    if (isset($rel[0]) && $rel[0] === '/') {
        return $p['scheme'] . '://' . $p['host'] . (isset($p['port']) ? ':' . $p['port'] : '') . $rel;
    }

    $path = $p['path'] ?? '/';
    $dir = preg_replace('~/[^/]*$~', '/', $path);
    return $p['scheme'] . '://' . $p['host'] . (isset($p['port']) ? ':' . $p['port'] : '') . $dir . $rel;
}
