<?php
/**
 * IPTV proxy — pure PHP / cURL implementation.
 *
 *   GET  /api/iptv-proxy?url=...&ua=...      (used by <video> / hls.js)
 *   POST /api/iptv-proxy  { url, responseType, ua? }   (used by Xtream/M3U parsers)
 *
 * Mirrors the behaviour of the original Python proxy:
 *   - rewrites HLS playlists so segments route back through /api/iptv-proxy
 *   - blocks private/loopback addresses
 *   - forwards Range header for seeking
 *   - streams binary bodies with output-buffering off so PHP doesn't OOM
 *     on big VOD files
 */

const DEFAULT_UA =
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
    '(KHTML, like Gecko) Chrome/124.0 Safari/537.36';

function _is_private_host(string $host): bool {
    $h = strtolower($host);
    if (in_array($h, ['localhost', '0.0.0.0', '::1'], true)) return true;
    if (str_ends_with($h, '.local') || str_ends_with($h, '.internal')) return true;
    $ip = filter_var($h, FILTER_VALIDATE_IP);
    if ($ip === false) return false;
    return !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}

function _validate_url(?string $raw): ?string {
    if (!$raw) return null;
    $p = parse_url($raw);
    if (!$p || !isset($p['scheme'], $p['host'])) return null;
    if (!in_array($p['scheme'], ['http', 'https'], true)) return null;
    if (_is_private_host($p['host'])) return null;
    return $raw;
}

function _looks_like_hls(string $text): bool {
    if ($text === '') return false;
    $stripped = ltrim($text, "\xEF\xBB\xBF \t\r\n");
    return strncasecmp($stripped, '#EXTM3U', 7) === 0;
}

function _proxy_self_url(): string {
    $proto = $_SERVER['HTTP_X_FORWARDED_PROTO']
        ?? (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
    $host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? 'localhost';
    // Use the actual request path so rewrites stay under e.g. /nadiplayer/api/iptv-proxy
    $req_path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/api/iptv-proxy';
    // We need the path up to and including ".../iptv-proxy"
    $pos = strpos($req_path, '/iptv-proxy');
    $prefix = $pos !== false ? substr($req_path, 0, $pos + strlen('/iptv-proxy')) : '/api/iptv-proxy';
    return "$proto://$host$prefix";
}

function _abs_url(string $base, string $ref): string {
    if (preg_match('#^https?://#i', $ref)) return $ref;
    $bp = parse_url($base);
    $origin = $bp['scheme'] . '://' . $bp['host'] . (isset($bp['port']) ? ':' . $bp['port'] : '');
    if (str_starts_with($ref, '/')) return $origin . $ref;
    $path = $bp['path'] ?? '/';
    $dir  = substr($path, 0, strrpos($path, '/') + 1);
    return $origin . $dir . $ref;
}

function _rewrite_m3u8(string $text, string $base_url, string $ua): string {
    $self_url = _proxy_self_url();
    $ua_qs = ($ua && $ua !== DEFAULT_UA) ? '&ua=' . rawurlencode($ua) : '';
    $out = [];
    foreach (preg_split('/\r?\n/', $text) as $line) {
        $trim = trim($line);
        if ($trim === '') { $out[] = $line; continue; }
        if ($trim[0] === '#') {
            $line = preg_replace_callback(
                '/URI="([^"]+)"/',
                function ($m) use ($base_url, $self_url, $ua_qs) {
                    $abs = _abs_url($base_url, $m[1]);
                    return 'URI="' . $self_url . '?url='
                         . rawurlencode($abs) . $ua_qs . '"';
                },
                $line
            );
            $out[] = $line;
            continue;
        }
        $abs = _abs_url($base_url, $trim);
        $out[] = $self_url . '?url=' . rawurlencode($abs) . $ua_qs;
    }
    return implode("\n", $out);
}

/** Fetch the full body (used for JSON / text / m3u8 manifests). */
function _curl_buffered(string $url, string $ua, ?string $range): array {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => array_filter([
            'User-Agent: ' . $ua,
            'Accept: */*',
            $range ? "Range: $range" : null,
        ]),
        CURLOPT_HEADER         => false,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $resp = curl_exec($ch);
    if ($resp === false) {
        $err = curl_error($ch);
        curl_close($ch);
        return [null, 0, '', $err];
    }
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $ct     = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'application/octet-stream';
    curl_close($ch);
    return [$resp, $status, $ct, null];
}

/** Stream the body straight to the client (used for video/audio). */
function _curl_stream(string $url, string $ua, ?string $range): void {
    // Disable any output buffering so chunks flush immediately.
    while (ob_get_level()) { ob_end_clean(); }
    @set_time_limit(0);
    @ignore_user_abort(false);

    $headers_sent = false;
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 0,
        CURLOPT_HTTPHEADER     => array_filter([
            'User-Agent: ' . $ua,
            'Accept: */*',
            $range ? "Range: $range" : null,
        ]),
        CURLOPT_BUFFERSIZE     => 65536,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_HEADERFUNCTION => function ($ch, $h) use (&$headers_sent) {
            if (!$headers_sent) {
                $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                if ($status > 0) {
                    http_response_code($status);
                    $headers_sent = true;
                }
            }
            $low = strtolower($h);
            foreach (['content-type:', 'content-length:', 'content-range:',
                      'accept-ranges:', 'cache-control:'] as $allowed) {
                if (str_starts_with($low, $allowed)) {
                    header(rtrim($h, "\r\n"), false);
                    break;
                }
            }
            return strlen($h);
        },
        CURLOPT_WRITEFUNCTION => function ($ch, $chunk) {
            echo $chunk;
            @flush();
            if (connection_aborted()) return -1;
            return strlen($chunk);
        },
    ]);
    @header('x-proxy-kind: passthrough');
    curl_exec($ch);
    curl_close($ch);
    exit;
}

function _handle_proxy(string $target_raw, string $response_type, ?string $ua): void {
    $target = _validate_url($target_raw);
    if (!$target) json_error(400, 'Invalid or blocked URL');

    $headers = function_exists('getallheaders') ? getallheaders() : [];
    $upstream_ua = $ua
        ?: ($headers['x-upstream-ua'] ?? $headers['X-Upstream-Ua'] ?? null)
        ?: DEFAULT_UA;

    $range = $_SERVER['HTTP_RANGE'] ?? null;

    $url_is_m3u8 = (bool)preg_match('/\.m3u8(\?|$)/i', $target);
    $may_be_m3u8 = $url_is_m3u8;     // we decide for sure after content-type

    if ($may_be_m3u8 || in_array($response_type, ['json', 'text'], true)) {
        [$body, $status, $ct, $err] = _curl_buffered($target, $upstream_ua, $range);
        if ($err !== null) {
            http_response_code(502);
            header('Content-Type: text/plain; charset=utf-8');
            echo "Upstream fetch failed: $err";
            exit;
        }
        $ct_is_m3u8 = (bool)preg_match('/mpegurl|m3u8|x-mpegurl/i', $ct);
        $ct_is_text = (bool)preg_match('#^text/|\+xml|json|plain#i', $ct);
        $is_manifest = $ct_is_m3u8 || ($url_is_m3u8 && $ct_is_text);

        if ($response_type === 'json') {
            http_response_code($status);
            header("Content-Type: $ct");
            header('x-proxy-kind: json-passthrough');
            echo $body;
            exit;
        }

        if ($is_manifest && _looks_like_hls($body)) {
            $rewritten = _rewrite_m3u8($body, $target, $upstream_ua);
            http_response_code($status);
            header('Content-Type: application/vnd.apple.mpegurl');
            header('Cache-Control: no-cache');
            header('x-proxy-kind: m3u8');
            echo $rewritten;
            exit;
        }

        http_response_code($status);
        header("Content-Type: $ct");
        header('Cache-Control: no-cache');
        header('x-proxy-kind: text-passthrough');
        echo $body;
        exit;
    }

    // Binary / video passthrough
    _curl_stream($target, $upstream_ua, $range);
}

function route_iptv_proxy_get(): void {
    $url = $_GET['url'] ?? null;
    $ua  = $_GET['ua']  ?? null;
    if (!$url) json_error(400, "Missing 'url' query parameter");
    _handle_proxy($url, 'stream', $ua);
}

function route_iptv_proxy_post(): void {
    $b = read_json_body();
    $url = require_field($b, 'url');
    $rt  = $b['responseType'] ?? 'json';
    if (!in_array($rt, ['json', 'text', 'stream'], true)) json_error(400, 'Bad responseType');
    $ua  = $b['ua'] ?? null;
    _handle_proxy($url, $rt, $ua);
}

function route_iptv_proxy_options(): void {
    send_cors_headers();
    send_json(['ok' => true]);
}
