<?php
require_once __DIR__ . '/../api_common.php';

// -----------------------------------------------------------------------------
// Segment requests are usually .ts bytes. Redirecting to arbitrary media types can
// break some players, so we only redirect here when the configured fail video
// ends with .ts (best compatibility).
// -----------------------------------------------------------------------------
function _seg_fail_video_url(PDO $pdo, string $type, string $reason): string {
  $kind = ($type === 'live') ? 'live' : 'vod';
  $primary = (string)system_setting_get($pdo, "fail_video_{$kind}_{$reason}", '');
  if ($primary !== '') return $primary;
  $other = ($kind === 'live') ? 'vod' : 'live';
  return (string)system_setting_get($pdo, "fail_video_{$other}_{$reason}", '');
}
function _seg_try_redirect_ts(string $url): void {
  if ($url === '') return;
  if (!preg_match('/\.ts(\?|$)/i', $url)) return;
  http_response_code(302);
  header('Cache-Control: no-store, no-cache, must-revalidate');
  header('Pragma: no-cache');
  header('Location: ' . $url);
  exit;
}

// Base64url decode helper (pairs with stream/index.php src=...)
function b64url_encode_str(string $s): string {
  return rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
}

function b64url_decode_str(string $s): string {
  $s = strtr($s, '-_', '+/');
  $pad = strlen($s) % 4;
  if ($pad) $s .= str_repeat('=', 4 - $pad);
  $out = base64_decode($s, true);
  return ($out === false) ? '' : $out;
}

$u  = (string)($_GET['u'] ?? '');
$p  = (string)($_GET['p'] ?? '');
$id = (int)($_GET['id'] ?? 0);
$type = strtolower((string)($_GET['type'] ?? 'live'));
if (!in_array($type, ['live','movie','episode'], true)) { http_response_code(400); exit('Bad type'); }
$stoken = (string)($_GET['st'] ?? '');

$exp   = (int)($_GET['exp'] ?? 0);
$token = (string)($_GET['token'] ?? '');

$src = (string)($_GET['src'] ?? '');
$url = $src !== '' ? b64url_decode_str($src) : (string)($_GET['url'] ?? '');

if ($u==='' || $id<1 || $url==='') {
  http_response_code(400); exit("Bad params");
}

// Always available for both signed and DB-validated paths
$ip = get_client_ip();
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 250);
$device_fp = get_device_fingerprint();
if ($device_fp==='' && strict_device_id_enabled()) { http_response_code(403); exit('device_id_required'); }

// Fast-path: signed segment URLs (generated by stream/index.php). When valid, skip DB work.
$signed_ok = false;
$sig = (string)($_GET['sig'] ?? '');
if ($sig !== '') {
  $cfg = require __DIR__ . '/../config.php';
  $secret = (string)($cfg['secret_key'] ?? '');
  if ($secret !== '' && $src !== '') {
    $seg_cred = ($p !== '') ? $p : $token;
    $device_id_raw = (string)($_GET['device_id'] ?? '');
    $dfp_param = (string)($_GET['dfp'] ?? '');
    $payload = $u . "|" . $seg_cred . "|" . (string)$id . "|" . $type . "|" . $stoken . "|" . (string)$exp . "|" . $device_id_raw . "|" . $dfp_param . "|" . $src;
    $good = rtrim(strtr(base64_encode(hash_hmac('sha256', $payload, $secret, true)), '+/', '-_'), '=');
    if (!hash_equals($good, $sig)) {
      http_response_code(403); exit('bad_sig');
    }

// Prevent replay of old signed playlists after a subscription expires.
// You can disable this if you need max performance:
//   config.php: 'enforce_subscription_on_segments' => false
$enf = (bool)($cfg['enforce_subscription_on_segments'] ?? true);
if ($enf) {
  $pdo = db();
  $st = $pdo->prepare("SELECT id FROM users WHERE username=? AND status='active' LIMIT 1");
  $st->execute([$u]);
  $uid = (int)($st->fetch(PDO::FETCH_ASSOC)['id'] ?? 0);
  if ($uid > 0) {
    $ttl2 = (int)($cfg['sub_cache_ttl'] ?? 60);
    if ($ttl2 < 5) $ttl2 = 5;
    $sub2 = iptv_cached_active_subscription($pdo, $uid, $ttl2);
    if (!$sub2) {
      iptv_expire_due_subscriptions($pdo, $uid);
      $fv = _seg_fail_video_url($pdo, $type, 'expired');
      _seg_try_redirect_ts($fv);
      http_response_code(403);
      exit('Expired');
    }
  }
}
// If we're in token mode, enforce token validity. If the token is invalid/expired and the user
// subscription is also expired, redirect to the "expired" fail video (best-effort, .ts only).
if ($token !== '' && $exp > 0 && !verify_token($u, $id, $exp, $token, $type)) {
  $pdo = db();
  $st = $pdo->prepare("SELECT id FROM users WHERE username=? AND status='active' LIMIT 1");
  $st->execute([$u]);
  $uid = (int)($st->fetch(PDO::FETCH_ASSOC)['id'] ?? 0);
  if ($uid > 0) {
    $cfg2 = require __DIR__ . '/../config.php';
    $ttl2 = (int)($cfg2['sub_cache_ttl'] ?? 60);
    if ($ttl2 < 5) $ttl2 = 5;
    $sub2 = iptv_cached_active_subscription($pdo, $uid, $ttl2);
    if (!$sub2) {
      iptv_expire_due_subscriptions($pdo, $uid);
      $fv = _seg_fail_video_url($pdo, $type, 'expired');
      _seg_try_redirect_ts($fv);
      http_response_code(403);
      exit('Expired');
    }
  }
  // Subscription is still active (or unknown) but token is invalid/stale.
  $fv = _seg_fail_video_url($pdo, $type, 'invalid_login');
  _seg_try_redirect_ts($fv);
  http_response_code(401);
  exit('token_invalid');
}
    $signed_ok = true;
  } else {
    http_response_code(403); exit('bad_sig');
  }
}

if (!$signed_ok) {
  $pdo = db();
$ip = get_client_ip();
$ban = abuse_ip_ban_lookup($pdo, $ip);
if ($ban) {
  audit_log('ban_block', null, ['ban_type'=>'ip','ip'=>$ip]);
  $fv = _seg_fail_video_url($pdo, $type, 'banned');
  _seg_try_redirect_ts($fv);
  http_response_code(403);
  exit('Banned');
}

ensure_categories($pdo);

$ip = get_client_ip();
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 250);
$device_fp = get_device_fingerprint();
if ($device_fp==='' && strict_device_id_enabled()) { http_response_code(403); exit('device_id_required'); }

/* user */
$st = $pdo->prepare("SELECT * FROM users WHERE username=? AND status='active' LIMIT 1");
$st->execute([$u]);
$user = $st->fetch(PDO::FETCH_ASSOC);
if (!$user) {
  $fv = _seg_fail_video_url($pdo, $type, 'invalid_login');
  _seg_try_redirect_ts($fv);
  http_response_code(401);
  exit("Invalid user");
}

/* token OR password */
$token_ok = ($token && $exp && verify_token($u, $id, $exp, $token, $type));
$pass_ok  = ($p !== '' && password_verify($p, $user['password_hash']));
if (!$token_ok && !$pass_ok) {
  $fv = _seg_fail_video_url($pdo, $type, 'invalid_login');
  _seg_try_redirect_ts($fv);
  http_response_code(401);
  exit("Invalid credentials");
}


// Hard bans (user)
$ban = abuse_user_ban_lookup($pdo, (int)$user['id']);
if ($ban) {
  audit_log('ban_block_user', (int)$user['id'], ['ban_type'=>'user','ip'=>$ip]);
  $fv = _seg_fail_video_url($pdo, $type, 'banned');
  _seg_try_redirect_ts($fv);
  http_response_code(403);
  exit('Banned');
}


// Policy: IP allow/deny
if (!ip_allowed($ip, $user['ip_allowlist'] ?? null, $user['ip_denylist'] ?? null)) {
  $fv = _seg_fail_video_url($pdo, $type, 'banned');
  _seg_try_redirect_ts($fv);
  http_response_code(403);
  exit("IP not allowed");
}

// Active subscription enforcement (cached). Keeps behavior consistent even if an app
// replays old playlists / segment URLs after a sub expires.
$cfgs = require __DIR__ . '/../config.php';
$ttls = (int)($cfgs['sub_cache_ttl'] ?? 60);
if ($ttls < 5) $ttls = 5;
$subs = iptv_cached_active_subscription($pdo, (int)$user['id'], $ttls);
if (!$subs) {
  iptv_expire_due_subscriptions($pdo, (int)$user['id']);
  $fv = _seg_fail_video_url($pdo, $type, 'expired');
  _seg_try_redirect_ts($fv);
  http_response_code(403);
  exit('Expired');
}

/* adult filter */
if ($type === 'live') {
  $st = $pdo->prepare("SELECT IFNULL(is_adult,0) AS is_adult FROM channels WHERE id=?");
  $st->execute([$id]);
  $ch = $st->fetch(PDO::FETCH_ASSOC);
  if ($ch && empty($user['allow_adult']) && (int)$ch['is_adult']===1) {
    http_response_code(403); exit("Adult content not allowed");
  }
} elseif ($type === 'movie') {
  $st = $pdo->prepare("SELECT IFNULL(is_adult,0) AS is_adult FROM movies WHERE id=?");
  $st->execute([$id]);
  $ch = $st->fetch(PDO::FETCH_ASSOC);
  if ($ch && empty($user['allow_adult']) && (int)$ch['is_adult']===1) {
    http_response_code(403); exit("Adult content not allowed");
  }
} else { // episode
  $st = $pdo->prepare("SELECT IFNULL(s.is_adult,0) AS is_adult FROM series_episodes e JOIN series s ON s.id=e.series_id WHERE e.id=?");
  $st->execute([$id]);
  $ch = $st->fetch(PDO::FETCH_ASSOC);
  if ($ch && empty($user['allow_adult']) && (int)$ch['is_adult']===1) {
    http_response_code(403); exit("Adult content not allowed");
  }
}

/* package/bouquet enforcement (live + VOD + Series) */
$pkg_ids = user_package_ids($pdo, (int)$user['id']);

if ($pkg_ids) {
  $in = implode(',', array_fill(0, count($pkg_ids), '?'));

  if ($type === 'live') {
    $params = array_merge([$id], $pkg_ids);
    $st = $pdo->prepare("SELECT 1 FROM package_channels pc WHERE pc.channel_id=? AND pc.package_id IN ($in) LIMIT 1");
    $st->execute($params);
    if (!$st->fetch()) { http_response_code(403); exit("Not in your package"); }
  } elseif ($type === 'movie') {
    $params = array_merge([$id], $pkg_ids);
    $st = $pdo->prepare("SELECT 1 FROM package_movies pm WHERE pm.movie_id=? AND pm.package_id IN ($in) LIMIT 1");
    $st->execute($params);
    if (!$st->fetch()) { http_response_code(403); exit("Not in your package"); }
  } else { // episode -> package rules are on the parent series
    $st = $pdo->prepare("SELECT series_id FROM series_episodes WHERE id=? LIMIT 1");
    $st->execute([$id]);
    $series_id = (int)($st->fetch(PDO::FETCH_ASSOC)['series_id'] ?? 0);
    if ($series_id < 1) { http_response_code(404); exit('Episode not found'); }

    $params = array_merge([$series_id], $pkg_ids);
    $st = $pdo->prepare("SELECT 1 FROM package_series ps WHERE ps.series_id=? AND ps.package_id IN ($in) LIMIT 1");
    $st->execute($params);
    if (!$st->fetch()) { http_response_code(403); exit("Not in your package"); }
  }
}

/* Require an active recent session for this DEVICE (anti-hotlink + fixes channel switch) */
$config = require __DIR__ . '/../config.php';
$dev_win = (int)($config['device_window'] ?? 300);
if ($dev_win < 120) $dev_win = 120;
$seg_win = (int)($config['segment_window'] ?? max($dev_win * 6, 900));
if ($seg_win < $dev_win) $seg_win = $dev_win;
if ($stoken === '') {
  http_response_code(401);
  exit('missing_session_token');
}

try {
  if ($device_fp !== '') {
    $st = $pdo->prepare("
      SELECT id FROM stream_sessions
      WHERE user_id=?
        AND device_fp=?
        AND session_token=?
        AND (killed_at IS NULL OR killed_at='0000-00-00 00:00:00')
        AND last_seen > (NOW() - INTERVAL ? SECOND)
      ORDER BY last_seen DESC LIMIT 1
    ");
    $st->execute([(int)$user['id'], $device_fp, $stoken, $seg_win]);
  } else {
    $st = $pdo->prepare("
      SELECT id FROM stream_sessions
      WHERE user_id=?
        AND ip=?
        AND user_agent=?
        AND session_token=?
        AND (killed_at IS NULL OR killed_at='0000-00-00 00:00:00')
        AND last_seen > (NOW() - INTERVAL ? SECOND)
      ORDER BY last_seen DESC LIMIT 1
    ");
    $st->execute([(int)$user['id'], $ip, $ua, $stoken, $seg_win]);
  }

  $session = $st->fetch(PDO::FETCH_ASSOC);
  if (!$session) {
    http_response_code(429);
    header("Content-Type: application/json; charset=utf-8");
    echo json_encode(["error"=>"no_active_session"]);
    exit;
  }
} catch (Throwable $e) {
  http_response_code(500);
  exit('segment_session_check_failed');
}

// refresh last_seen
$pdo->prepare("UPDATE stream_sessions SET last_seen=NOW(), ip=?, user_agent=?, device_fp=? WHERE id=?")
    ->execute([$ip, $ua, $device_fp, (int)$session['id']]);
}
 /* let upstream dictate content-type */
// ---- streaming tune-ups (important for .ts segments) ----
@ini_set('zlib.output_compression', 'Off');
@ini_set('output_buffering', 'Off');
@ini_set('implicit_flush', '1');
if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); }
while (ob_get_level() > 0) { @ob_end_flush(); }
@ob_implicit_flush(true);
@set_time_limit(0);
@ignore_user_abort(true);
header('X-Accel-Buffering: no'); // disables nginx proxy buffering when supported
header_remove("Content-Type");

/* ---------- playlist proxy (nested .m3u8) ---------- */
$is_playlist = (bool)preg_match('/\.m3u8(\?|$)/i', $url) || (bool)preg_match('/\.m3u(\?|$)/i', $url);
if ($is_playlist) {
  // For playlists, do NOT redirect to fail videos here (players expect text/m3u8).
  header_remove("Content-Type");
  header('Content-Type: application/vnd.apple.mpegurl; charset=utf-8');
  header('Cache-Control: no-store, no-cache, must-revalidate');
  header('Pragma: no-cache');

  $config   = require __DIR__ . '/../config.php';
  $base_url = rtrim($config['base_url'], '/');

  // Build seg base URL matching the current auth mode
  $seg_cred = ($p !== '') ? $p : $token;
  $seg_base = $base_url . "/seg/" . rawurlencode($u) . "/" . rawurlencode($seg_cred) . "/" . $id;

  // Pass-through params required by segment validator
  $pass = [
    'type' => $type,
    'st'   => $stoken,
  ];
  if ($exp > 0) $pass['exp'] = (string)$exp;
  if (isset($_GET['device_id']) && (string)$_GET['device_id'] !== '') $pass['device_id'] = (string)$_GET['device_id'];
  if (isset($_GET['dfp']) && (string)$_GET['dfp'] !== '') $pass['dfp'] = (string)$_GET['dfp'];
  if ($token !== '') $pass['token'] = $token;

  // Resolve relative URLs against this playlist URL
  $pl_parsed = parse_url($url);
  $pl_scheme = $pl_parsed['scheme'] ?? 'http';
  $pl_host   = $pl_parsed['host'] ?? '';
  $pl_port   = isset($pl_parsed['port']) ? (':' . $pl_parsed['port']) : '';
  $pl_path   = $pl_parsed['path'] ?? '/';
  $pl_dir    = preg_replace('~/[^/]*$~', '/', $pl_path);
  $pl_origin = $pl_scheme . '://' . $pl_host . $pl_port;

  $resolve = function(string $ref) use ($pl_scheme, $pl_origin, $pl_dir): string {
    $ref = trim($ref);
    if ($ref === '' || stripos($ref, 'data:') === 0) return $ref;
    if (preg_match('~^https?://~i', $ref)) return $ref;
    if (strpos($ref, '//') === 0) return $pl_scheme . ':' . $ref;
    if (strpos($ref, '/') === 0) return $pl_origin . $ref;
    return $pl_origin . $pl_dir . $ref;
  };

  $wrap = function(string $abs) use ($seg_base, $pass, $config, $u, $seg_cred, $id, $type): string {
    $abs = trim($abs);
    if ($abs === '') return $abs;

    // Idempotent: if it's already our seg URL with src=, keep it
    if (strpos($abs, '/seg/') !== false && (strpos($abs, 'src=') !== false || strpos($abs, '?src=') !== false)) {
      return $abs;
    }

    $src_enc = b64url_encode_str($abs);
    $q = 'src=' . rawurlencode($src_enc);
    $tail = http_build_query($pass, '', '&', PHP_QUERY_RFC3986);
    if ($tail !== '') $q .= '&' . $tail;

    // Signed segment URLs: lets segment.php skip DB lookups (massive load reduction on shared hosting)
    if (!empty($config['secret_key'])) {
      $st = (string)($pass['st'] ?? '');
      $expv = (string)($pass['exp'] ?? 0);
      $device_id_raw = (string)($pass['device_id'] ?? '');
      $dfp_param = (string)($pass['dfp'] ?? '');
      $payload = $u . "|" . $seg_cred . "|" . (string)$id . "|" . $type . "|" . $st . "|" . $expv . "|" . $device_id_raw . "|" . $dfp_param . "|" . $src_enc;
      $sig = rtrim(strtr(base64_encode(hash_hmac('sha256', $payload, $config['secret_key'], true)), '+/', '-_'), '=');
      $q .= '&sig=' . rawurlencode($sig);
    }

    return $seg_base . '?' . $q;
  };

  /* ---------- fetch upstream playlist ---------- */
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false,
    CURLOPT_ENCODING => "",
    CURLOPT_USERAGENT => $ua,
    CURLOPT_HTTPHEADER => [
      'Accept: application/vnd.apple.mpegurl, application/x-mpegURL, */*',
    ],
  ]);
  $body = curl_exec($ch);
  $err  = curl_error($ch);
  $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  curl_close($ch);

  if ($body === false) {
    http_response_code(502);
    exit('upstream_error');
  }
  if ($code >= 400) {
    http_response_code($code);
    // Return a minimal playlist so players stop cleanly.
    echo "#EXTM3U\n#EXT-X-ENDLIST\n";
    exit;
  }

  $lines = preg_split("/\r\n|\n|\r/", (string)$body);
  $out = [];
  foreach ($lines as $ln) {
    $ln = (string)$ln;

    if ($ln === '' || $ln[0] === '#') {
      // Rewrite URI="..." fields inside tags (KEY/MAP/etc)
      $ln = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($resolve, $wrap) {
        $abs = $resolve((string)$m[1]);
        return 'URI="' . $wrap($abs) . '"';
      }, $ln);
      $out[] = $ln;
      continue;
    }

    $abs = $resolve($ln);
    $out[] = $wrap($abs);
  }

  echo implode("\n", $out);
  exit;
}


header('Content-Type: video/mp2t');

/* ---------- stream segment bytes ---------- */
$ch = curl_init($url);
$headers = [
  'Accept: */*',
  'Connection: keep-alive'
];
if (!empty($_SERVER['HTTP_REFERER'])) $headers[] = 'Referer: '.$_SERVER['HTTP_REFERER'];
if (!empty($_SERVER['HTTP_ORIGIN']))  $headers[] = 'Origin: '.$_SERVER['HTTP_ORIGIN'];
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) $headers[] = 'Accept-Language: '.$_SERVER['HTTP_ACCEPT_LANGUAGE'];

if (!empty($_SERVER['HTTP_RANGE'])) $headers[] = 'Range: '.$_SERVER['HTTP_RANGE'];

curl_setopt_array($ch, [
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_RETURNTRANSFER => false,
  CURLOPT_SSL_VERIFYPEER => false,
  CURLOPT_SSL_VERIFYHOST => false,
  CURLOPT_ENCODING => "",
  CURLOPT_USERAGENT => $_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0',
  CURLOPT_HTTPHEADER => $headers,
  CURLOPT_TIMEOUT => 20,
  CURLOPT_HEADERFUNCTION => function($curl, $header) {
    $len = strlen($header);
// Forward upstream HTTP status (helps players fail fast on 403/404)
if (preg_match('#^HTTP/\S+\s+(\d{3})#i', $header, $mm)) {
  http_response_code((int)$mm[1]);
}
    if (stripos($header, 'Content-Type:') === 0) header(trim($header));
    if (stripos($header, 'Content-Length:') === 0) header(trim($header));
    if (stripos($header, 'Accept-Ranges:') === 0) header(trim($header));
    if (stripos($header, 'Content-Range:') === 0) header(trim($header));
    return $len;
  }
]);
curl_exec($ch);
curl_close($ch);
