<?php
/**
 * StreamVault Backend Engine v2.0
 * ─────────────────────────────────────────────────────────────────────────────
 * Plugin-compatible IPTV/VOD engine. All persistent state lives in iptv.json.
 *
 * NEW in v2:
 *  • EPG tag per network (epg_url in each network json) — fetches XMLTV or JSON,
 *    parses & caches schedule into iptv.json["epg"] keyed by channel id
 *  • Logo resolution from iptv-org CDN saved into iptv.json channel cache
 *  • EPG cache TTL separate from channel cache (default 12h)
 *  • ip-api.com geo-detect endpoint
 *  • mjh.nz playlist auto-sync endpoint
 *
 * Endpoints:
 *  ?action=channels              → all channels (merged + logo-resolved, cached)
 *  ?action=networks              → network summaries
 *  ?action=network&id=nz.json   → single network channels
 *  ?action=categories            → unique category list
 *  ?action=search&q=news         → full-text search
 *  ?action=refresh               → force rebuild channel cache
 *  ?action=epg&channel=<id>      → EPG for one channel (from cache)
 *  ?action=epg_refresh           → re-fetch & cache all EPG sources
 *  ?action=epg_refresh&network=nz.json → re-fetch single network EPG
 *  ?action=geo                   → detect visitor region via ip-api.com
 *  ?action=plugins               → list loaded plugins
 *  ?action=health                → system health check
 */

header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Cache-Control: no-cache');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }

// ─── CONFIG ──────────────────────────────────────────────────────────────────
define('BASE_DIR',       __DIR__);
define('JSON_DIR',       BASE_DIR . '/json/');
define('NETWORKS_FILE',  BASE_DIR . '/networks.json');
define('IPTV_CACHE',     BASE_DIR . '/iptv.json');
define('PLUGINS_DIR',    BASE_DIR . '/plugins/');
define('CHANNEL_TTL',    3600);    // 1 hour  — channel list cache
define('EPG_TTL',        43200);   // 12 hours — EPG schedule cache
define('LOGO_CDN',       'https://iptv-org.github.io/iptv/logo/');
define('GEO_API',        'https://ip-api.com/json/?fields=country,countryCode,regionName,city,status');
define('REQUEST_TIMEOUT', 10);     // seconds for remote fetches

// ─── PLUGIN LOADER ───────────────────────────────────────────────────────────
$loadedPlugins = [];
function loadPlugins(): array {
    global $loadedPlugins;
    if (!is_dir(PLUGINS_DIR)) return [];
    foreach (glob(PLUGINS_DIR . '*.php') as $file) {
        $name = basename($file, '.php');
        if (!isset($loadedPlugins[$name])) {
            include_once $file;
            $loadedPlugins[$name] = [
                'file'    => $file,
                'loaded'  => date('c'),
                'version' => defined("PLUGIN_{$name}_VERSION") ? constant("PLUGIN_{$name}_VERSION") : '1.0',
            ];
        }
    }
    return $loadedPlugins;
}

// ─── HTTP HELPER ─────────────────────────────────────────────────────────────
function remoteGet(string $url, int $timeout = REQUEST_TIMEOUT): ?string {
    $ctx = stream_context_create(['http' => [
        'timeout' => $timeout,
        'header'  => "User-Agent: StreamVault/2.0\r\n",
        'ignore_errors' => true,
    ]]);
    $body = @file_get_contents($url, false, $ctx);
    return ($body !== false) ? $body : null;
}

// ─── CACHE HELPERS ───────────────────────────────────────────────────────────
function readCache(): array {
    if (!file_exists(IPTV_CACHE)) return [];
    $d = json_decode(file_get_contents(IPTV_CACHE), true);
    return is_array($d) ? $d : [];
}

function writeCache(array $data): void {
    file_put_contents(IPTV_CACHE, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}

function cacheAge(array $cache, string $key = 'lastUpdated'): int {
    if (empty($cache[$key])) return PHP_INT_MAX;
    return time() - strtotime($cache[$key]);
}

// ─── LOGO RESOLUTION ─────────────────────────────────────────────────────────
// Tries to confirm logo URL. Falls back to iptv-org CDN slug if needed.
// Result is stored in cache so we don't re-check on every request.
function resolveLogoUrl(string $channelId, string $existingLogo): string {
    if ($existingLogo) return $existingLogo; // trust what's in the json
    // Try iptv-org CDN by channel id slug
    $slug = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $channelId));
    $cdn  = LOGO_CDN . $slug . '.png';
    // We can't easily HEAD-check in PHP without curl; just return the CDN url as fallback
    return $cdn;
}

// ─── NETWORK FILE READER ─────────────────────────────────────────────────────
function readNetworkFile(string $filename): array {
    $path = JSON_DIR . basename($filename);
    if (!file_exists($path)) return [];
    $data = json_decode(file_get_contents($path), true);
    if (!$data || !isset($data['channels'])) return [];

    foreach ($data['channels'] as &$ch) {
        $ch['network']    = $data['network'] ?? 'Unknown';
        $ch['network_id'] = $filename;
        $ch['region']     = $data['region']  ?? '';
        $ch['flag']       = $data['flag']    ?? '';
        $ch['netColor']   = $data['color']   ?? '#7b6cff';
        $ch['epg_id']     = $ch['epg_id']    ?? $ch['id'] ?? '';
        // Resolve logo (uses existing or CDN fallback)
        $ch['logo']       = resolveLogoUrl($ch['id'] ?? '', $ch['logo'] ?? '');
    }
    return $data['channels'];
}

function getNetworkMeta(string $filename): array {
    $path = JSON_DIR . basename($filename);
    if (!file_exists($path)) return [];
    $data = json_decode(file_get_contents($path), true);
    return [
        'file'    => $filename,
        'name'    => $data['network'] ?? $filename,
        'region'  => $data['region'] ?? '',
        'flag'    => $data['flag']   ?? '',
        'color'   => $data['color']  ?? '#7b6cff',
        'epg_url' => $data['epg_url'] ?? '',   // ← NEW: per-network EPG source
        'count'   => count($data['channels'] ?? []),
    ];
}

function getNetworkList(): array {
    if (!file_exists(NETWORKS_FILE)) apiError('networks.json not found', 404);
    $data = json_decode(file_get_contents(NETWORKS_FILE), true);
    return $data['files'] ?? [];
}

// ─── CHANNEL CACHE ───────────────────────────────────────────────────────────
function buildChannelCache(bool $force = false): array {
    $cache = readCache();

    if (!$force && !empty($cache['channels']) && cacheAge($cache) < CHANNEL_TTL) {
        return $cache;
    }

    $files       = getNetworkList();
    $allChannels = [];
    $allNetworks = [];
    $categories  = [];

    foreach ($files as $file) {
        $channels = readNetworkFile($file);
        foreach ($channels as $ch) {
            $allChannels[] = $ch;
            if (!empty($ch['category'])) $categories[$ch['category']] = true;
        }
        $meta = getNetworkMeta($file);
        if ($meta) $allNetworks[] = $meta;
    }

    if (function_exists('plugin_filter_channels')) {
        $allChannels = plugin_filter_channels($allChannels);
    }

    // Preserve existing EPG cache — don't wipe it on channel refresh
    $existingEpg    = $cache['epg']            ?? [];
    $existingEpgAge = $cache['epgLastUpdated'] ?? null;

    $cache = array_merge($cache, [
        'version'        => '2.0.0',
        'lastUpdated'    => date('c'),
        'channels'       => $allChannels,
        'categories'     => array_keys($categories),
        'networks'       => $allNetworks,
        'total'          => count($allChannels),
        'epg'            => $existingEpg,
        'epgLastUpdated' => $existingEpgAge,
    ]);

    writeCache($cache);
    return $cache;
}

// ─── EPG ENGINE ──────────────────────────────────────────────────────────────

/**
 * Parse XMLTV format into our schedule array.
 * Returns [ channelId => [ { start, stop, title, desc, category } ] ]
 */
function parseXMLTV(string $xml): array {
    $schedules = [];
    try {
        $dom = new DOMDocument();
        libxml_use_internal_errors(true);
        $dom->loadXML($xml);
        libxml_clear_errors();
        $xp = new DOMXPath($dom);

        foreach ($xp->query('//programme') as $prog) {
            $chId  = $prog->getAttribute('channel');
            $start = $prog->getAttribute('start');
            $stop  = $prog->getAttribute('stop');
            $title = $xp->query('title', $prog)->item(0)?->textContent ?? '';
            $desc  = $xp->query('desc',  $prog)->item(0)?->textContent ?? '';
            $cat   = $xp->query('category', $prog)->item(0)?->textContent ?? '';

            $schedules[$chId][] = [
                'start'    => normalizeEpgTime($start),
                'stop'     => normalizeEpgTime($stop),
                'title'    => $title,
                'desc'     => mb_substr($desc, 0, 300),
                'category' => $cat,
            ];
        }
    } catch (Throwable $e) { /* malformed XML — return empty */ }
    return $schedules;
}

/**
 * Parse JSON EPG format (common in some free APIs).
 * Supports both array-of-programmes and { channels: { id: [...] } } shapes.
 */
function parseJSONEPG(string $json): array {
    $schedules = [];
    $data = json_decode($json, true);
    if (!$data) return [];

    // Shape 1: { "channels": { "id": [ {start,stop,title,desc} ] } }
    if (isset($data['channels']) && is_array($data['channels'])) {
        foreach ($data['channels'] as $chId => $progs) {
            foreach ($progs as $p) {
                $schedules[$chId][] = [
                    'start'    => $p['start']    ?? $p['startTime'] ?? '',
                    'stop'     => $p['stop']     ?? $p['endTime']   ?? '',
                    'title'    => $p['title']    ?? '',
                    'desc'     => mb_substr($p['description'] ?? $p['desc'] ?? '', 0, 300),
                    'category' => $p['category'] ?? '',
                ];
            }
        }
        return $schedules;
    }

    // Shape 2: flat array of programmes with "channel" field
    if (isset($data[0]['channel'])) {
        foreach ($data as $p) {
            $chId = $p['channel'] ?? '';
            if (!$chId) continue;
            $schedules[$chId][] = [
                'start'    => $p['start'] ?? '',
                'stop'     => $p['stop']  ?? '',
                'title'    => $p['title'] ?? '',
                'desc'     => mb_substr($p['description'] ?? $p['desc'] ?? '', 0, 300),
                'category' => $p['category'] ?? '',
            ];
        }
    }
    return $schedules;
}

function normalizeEpgTime(string $t): string {
    // XMLTV: "20240524120000 +1200" → ISO 8601
    if (preg_match('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\s*([+-]\d{4})?$/', trim($t), $m)) {
        $tz = isset($m[7]) ? $m[7] : '+0000';
        $tz = substr($tz, 0, 3) . ':' . substr($tz, 3);
        return "{$m[1]}-{$m[2]}-{$m[3]}T{$m[4]}:{$m[5]}:{$m[6]}{$tz}";
    }
    return $t;
}

/**
 * Fetch EPG for one network file, merge into master epg cache.
 */
function fetchNetworkEPG(string $filename, array &$epgCache): array {
    $path = JSON_DIR . basename($filename);
    if (!file_exists($path)) return ['error' => 'file not found'];
    $data = json_decode(file_get_contents($path), true);
    $epgUrl = trim($data['epg_url'] ?? '');

    if (!$epgUrl) return ['skipped' => true, 'reason' => 'no epg_url defined'];

    $body = remoteGet($epgUrl, 30);
    if ($body === null) return ['error' => "failed to fetch $epgUrl"];

    // Detect format
    $parsed = [];
    $trimmed = ltrim($body);
    if (str_starts_with($trimmed, '<') || str_starts_with($trimmed, '<?')) {
        $parsed = parseXMLTV($body);
        $format = 'xmltv';
    } else {
        $parsed = parseJSONEPG($body);
        $format = 'json';
    }

    // Build a map of epg_id → channel id for this network
    $epgIdMap = [];
    foreach ($data['channels'] ?? [] as $ch) {
        $eid = $ch['epg_id'] ?? $ch['id'] ?? '';
        if ($eid) $epgIdMap[$eid] = $ch['id'];
    }

    $merged = 0;
    foreach ($parsed as $epgId => $progs) {
        // Try exact match then mapped id
        $chId = $epgIdMap[$epgId] ?? $epgId;
        $epgCache[$chId] = $progs;
        $merged++;
    }

    return [
        'network'    => $data['network'] ?? $filename,
        'epg_url'    => $epgUrl,
        'format'     => $format,
        'channels'   => $merged,
        'fetched_at' => date('c'),
    ];
}

function refreshAllEPG(bool $forceNetwork = false, ?string $singleFile = null): array {
    $cache  = buildChannelCache(); // ensure channel cache is fresh
    $epg    = $cache['epg'] ?? [];
    $files  = $singleFile ? [$singleFile] : getNetworkList();
    $report = [];

    foreach ($files as $file) {
        $report[$file] = fetchNetworkEPG($file, $epg);
    }

    $cache['epg']            = $epg;
    $cache['epgLastUpdated'] = date('c');
    writeCache($cache);

    return $report;
}

function getChannelEPG(string $channelId): array {
    // Plugin hook first
    if (function_exists('plugin_get_epg')) {
        $result = plugin_get_epg($channelId);
        if (!empty($result['schedule'])) return $result;
    }

    $cache = readCache();
    $epg   = $cache['epg'] ?? [];
    $schedule = $epg[$channelId] ?? [];

    // Filter to only current + upcoming (not old entries)
    $now = time();
    $schedule = array_values(array_filter($schedule, function($p) use ($now) {
        $stop = $p['stop'] ? strtotime($p['stop']) : 0;
        return !$stop || $stop > $now;
    }));

    // Sort by start time
    usort($schedule, fn($a,$b) => strtotime($a['start']??'') <=> strtotime($b['start']??''));

    $epgAge = $cache['epgLastUpdated'] ? (time() - strtotime($cache['epgLastUpdated'])) : null;

    return [
        'channel'      => $channelId,
        'schedule'     => array_slice($schedule, 0, 48), // next 48 programmes
        'count'        => count($schedule),
        'epgUpdatedAt' => $cache['epgLastUpdated'] ?? null,
        'epgAge'       => $epgAge ? $epgAge . 's' : null,
        'source'       => empty($schedule) ? 'no_data' : 'cache',
    ];
}

// ─── SEARCH ──────────────────────────────────────────────────────────────────
function searchChannels(string $query, array $channels): array {
    $q = strtolower(trim($query));
    return array_values(array_filter($channels, fn($c) =>
        str_contains(strtolower($c['name']        ?? ''), $q) ||
        str_contains(strtolower($c['category']    ?? ''), $q) ||
        str_contains(strtolower($c['network']     ?? ''), $q) ||
        str_contains(strtolower($c['description'] ?? ''), $q) ||
        str_contains(strtolower($c['region']      ?? ''), $q)
    ));
}

// ─── GEO DETECT ──────────────────────────────────────────────────────────────
function detectGeo(): array {
    $body = remoteGet(GEO_API, 5);
    if (!$body) return ['error' => 'geo lookup failed'];
    $data = json_decode($body, true);
    if (($data['status'] ?? '') !== 'success') return ['error' => 'geo lookup error'];

    // Map country code → region used in our network json files
    $regionMap = [
        'NZ' => 'NZ', 'AU' => 'NZ',
        'GB' => 'UK', 'IE' => 'UK',
        'US' => 'US', 'CA' => 'US',
    ];

    return [
        'country'     => $data['country']     ?? '',
        'countryCode' => $data['countryCode'] ?? '',
        'region'      => $data['regionName']  ?? '',
        'city'        => $data['city']        ?? '',
        'svRegion'    => $regionMap[$data['countryCode'] ?? ''] ?? 'US',
    ];
}

// ─── API HELPERS ─────────────────────────────────────────────────────────────
function apiResponse(array $data): void {
    echo json_encode(array_merge(['ok' => true], $data), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}
function apiError(string $msg, int $code = 400): void {
    http_response_code($code);
    echo json_encode(['ok' => false, 'error' => $msg]);
    exit;
}

// ─── ROUTER ──────────────────────────────────────────────────────────────────
loadPlugins();
$action = $_GET['action'] ?? 'channels';

switch ($action) {

    case 'channels':
        $cache    = buildChannelCache();
        $channels = $cache['channels'];
        if ($r = ($_GET['region']   ?? '')) $channels = array_values(array_filter($channels, fn($c) => strcasecmp($c['region']   ?? '', $r) === 0));
        if ($c = ($_GET['category'] ?? '')) $channels = array_values(array_filter($channels, fn($c2) => strcasecmp($c2['category'] ?? '', $c) === 0));
        if ($t = ($_GET['type']     ?? '')) $channels = array_values(array_filter($channels, fn($c) => strcasecmp($c['type']     ?? '', $t) === 0));
        apiResponse(['channels' => $channels, 'total' => count($channels), 'lastUpdated' => $cache['lastUpdated']]);

    case 'networks':
        $cache = buildChannelCache();
        apiResponse(['networks' => $cache['networks']]);

    case 'network':
        $id = $_GET['id'] ?? '';
        if (!$id) apiError('Missing ?id=filename.json');
        $channels = readNetworkFile($id);
        if (empty($channels)) apiError("Network file not found or empty: $id", 404);
        apiResponse(['channels' => $channels, 'total' => count($channels)]);

    case 'categories':
        $cache = buildChannelCache();
        apiResponse(['categories' => $cache['categories']]);

    case 'search':
        $q = $_GET['q'] ?? '';
        if (!$q) apiError('Missing ?q=query');
        $cache   = buildChannelCache();
        $results = searchChannels($q, $cache['channels']);
        apiResponse(['results' => $results, 'total' => count($results), 'query' => $q]);

    case 'refresh':
        $cache = buildChannelCache(true);
        apiResponse(['message' => 'Channel cache refreshed', 'total' => $cache['total'], 'networks' => count($cache['networks'])]);

    case 'epg':
        $id = $_GET['channel'] ?? '';
        if (!$id) apiError('Missing ?channel=id');
        apiResponse(getChannelEPG($id));

    case 'epg_refresh':
        set_time_limit(120);
        $single = $_GET['network'] ?? null;
        $report = refreshAllEPG(true, $single);
        apiResponse(['message' => 'EPG refreshed', 'report' => $report]);

    case 'geo':
        apiResponse(detectGeo());

    case 'plugins':
        global $loadedPlugins;
        apiResponse(['plugins' => $loadedPlugins, 'dir' => PLUGINS_DIR]);

    case 'health':
        $cache  = readCache();
        $epgAge = $cache['epgLastUpdated'] ? (time() - strtotime($cache['epgLastUpdated'])) : null;
        apiResponse([
            'status'         => 'ok',
            'channels'       => $cache['total']    ?? 0,
            'networks'       => count($cache['networks'] ?? []),
            'epgChannels'    => count($cache['epg']      ?? []),
            'cacheAge'       => $cache['lastUpdated']    ? (time()-strtotime($cache['lastUpdated'])).'s' : 'none',
            'epgAge'         => $epgAge !== null ? $epgAge.'s' : 'none',
            'channelTTL'     => CHANNEL_TTL.'s',
            'epgTTL'         => EPG_TTL.'s',
            'pluginsDir'     => PLUGINS_DIR,
            'plugins'        => count($loadedPlugins),
            'jsonDir'        => JSON_DIR,
            'php'            => PHP_VERSION,
        ]);

    default:
        apiError("Unknown action: $action");
}
