<?php
// sat.thedarkelite.com - TLE proxy + cache (fast + stale fallback)
//
// CelesTrak provides 3-line sets (name + line1 + line2).[6](https://celestrak.org/norad/documentation/tle-fmt.php)
// CelesTrak recommends updated access patterns for GP data and supports multiple formats.[2](https://www.celestrak.org/NORAD/documentation/gp-data-formats.php)[1](https://celestrak.org/NORAD/elements/)

header('Content-Type: application/json; charset=utf-8');

$start = microtime(true);
$group = isset($_GET['group']) ? strtolower(trim($_GET['group'])) : 'starlink';

function gpUrl(string $group, string $format = 'tle'): string {
  // Modern group endpoint (preferred for reliability)
  // See CelesTrak GP data format guidance + Current GP element sets index.[2](https://www.celestrak.org/NORAD/documentation/gp-data-formats.php)[1](https://celestrak.org/NORAD/elements/)
  return 'https://celestrak.org/NORAD/elements/gp.php?GROUP=' . rawurlencode($group) . '&FORMAT=' . rawurlencode($format);
}

// Use gp.php for groups that have shown 404s on static .txt,
// keep starlink static since it commonly exists and is huge.
$allowed = [
  'starlink' => ['name' => 'Starlink', 'url' => 'https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=tle'],
  'noaa' =>  ['name' => 'NOAA', 'url' => 'https://celestrak.org/NORAD/elements/gp.php?GROUP=noaa&FORMAT=tle'],
  'weather'  => ['name' => 'Weather',        'url' => gpUrl('weather', 'tle')],
  'active'   => ['name' => 'Active',         'url' => gpUrl('active', 'tle')],
  'stations' => ['name' => 'Space Stations', 'url' => gpUrl('stations', 'tle')],
];

if (!isset($allowed[$group])) {
  http_response_code(400);
  echo json_encode(['error' => 'Invalid group'], JSON_PRETTY_PRINT);
  exit;
}

$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) @mkdir($cacheDir, 0775, true);

$cacheKey  = preg_replace('/[^a-z0-9_\-]/', '_', $group);
$cacheFile = $cacheDir . "/tle_{$cacheKey}.json";

// TTL: 2 hours default (CelesTrak updates on a multi-hour cadence; frequent pulls can cause blocks).[7](https://usradioguy.com/wxtoimg-kepler-fix/)[1](https://celestrak.org/NORAD/elements/)
$ttlSeconds = 2 * 60 * 60;
$now = time();

$emitCache = function(array $cached, bool $isFresh) use ($ttlSeconds, $cacheFile, $now, $start) {
  $age = is_file($cacheFile) ? ($now - filemtime($cacheFile)) : null;
  $cached['meta']['cached'] = true;
  $cached['meta']['cacheAgeSeconds'] = $age;
  $cached['meta']['cacheTtlSeconds'] = $ttlSeconds;
  $cached['meta']['servedFrom'] = $isFresh ? 'cache_fresh' : 'cache_stale';
  $cached['meta']['serverMs'] = (int)round((microtime(true) - $start) * 1000);

  header('Cache-Control: public, max-age=60');
  echo json_encode($cached, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  exit;
};

// Serve fresh cache
if (is_file($cacheFile)) {
  $age = $now - filemtime($cacheFile);
  if ($age < $ttlSeconds) {
    $cached = json_decode(@file_get_contents($cacheFile), true);
    if (is_array($cached)) $emitCache($cached, true);
  }
}

// Fetch upstream (fail fast)
$url = $allowed[$group]['url'];
$ua  = 'sat.thedarkelite.com (TLE proxy; contact: admin@thedarkelite.com)';

$text = null;
$httpCode = 0;
$curlErr = null;

$connectTimeout = 4;
$totalTimeout   = 12;

if (function_exists('curl_init')) {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_CONNECTTIMEOUT => $connectTimeout,
    CURLOPT_TIMEOUT => $totalTimeout,
    CURLOPT_USERAGENT => $ua,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
    CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
    CURLOPT_ENCODING => '',
  ]);

  $text = curl_exec($ch);
  $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
  $curlErr = curl_error($ch);
  curl_close($ch);

  if ($text === false || $httpCode >= 400) {
    $text = null;
  }
}

if ($text === null) {
  // Serve stale cache if possible
  if (is_file($cacheFile)) {
    $cached = json_decode(@file_get_contents($cacheFile), true);
    if (is_array($cached)) {
      $cached['meta']['upstreamError'] = $curlErr ?: "Upstream fetch failed (HTTP $httpCode)";
      $cached['meta']['upstream'] = $url;
      $emitCache($cached, false);
    }
  }

  http_response_code(502);
  echo json_encode([
    'error' => 'Failed to fetch TLEs from upstream',
    'meta' => [
      'group' => $group,
      'upstream' => $url,
      'httpCode' => $httpCode,
      'curlError' => $curlErr,
      'serverMs' => (int)round((microtime(true) - $start) * 1000),
    ]
  ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  exit;
}

$parsed = parseTleText($text);

$fetchedAtUtc = gmdate('Y-m-d H:i:s');
$out = [
  'meta' => [
    'source' => 'CelesTrak',
    'group' => $group,
    'groupName' => $allowed[$group]['name'],
    'upstream' => $url,
    'cached' => false,
    'fetchedAtUtc' => $fetchedAtUtc,
    'cacheTtlSeconds' => $ttlSeconds,
    'count' => count($parsed),
    'upstreamHttpCode' => $httpCode,
    'serverMs' => (int)round((microtime(true) - $start) * 1000),
  ],
  'satellites' => $parsed
];

@file_put_contents($cacheFile, json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

header('Cache-Control: public, max-age=60');
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);


// ---- Helpers ----

function parseTleText(string $text): array {
  // TLE sets are typically name line + line1 + line2.[6](https://celestrak.org/norad/documentation/tle-fmt.php)
  $lines = preg_split("/\r\n|\n|\r/", trim($text));
  $lines = array_values(array_filter($lines, fn($l) => trim($l) !== ''));

  $out = [];
  $i = 0;

  while ($i < count($lines)) {
    $name = trim($lines[$i] ?? '');
    $l1 = trim($lines[$i+1] ?? '');
    $l2 = trim($lines[$i+2] ?? '');

    if ($name !== '' && $l1 !== '' && $l2 !== '' && $l1[0] === '1' && $l2[0] === '2') {
      // NORAD number is columns 03-07 in line 1.[6](https://celestrak.org/norad/documentation/tle-fmt.php)
      $norad = trim(substr($l1, 2, 5));
      if ($norad === '') $norad = '0';

      // Epoch year/day are in line1 columns 19-32.[6](https://celestrak.org/norad/documentation/tle-fmt.php)
      $epochRaw = trim(substr($l1, 18, 14));
      $epochStr = epochToIso($epochRaw);

      $out[] = [
        'name' => $name,
        'norad' => $norad,
        'line1' => $l1,
        'line2' => $l2,
        'epoch' => $epochStr
      ];
      $i += 3;
      continue;
    }

    $i += 1;
  }

  return $out;
}

function epochToIso(string $epochRaw): ?string {
  if ($epochRaw === '' || !preg_match('/^(\d{2})(\d{3}\.\d+)$/', $epochRaw, $m)) return null;

  $yy = intval($m[1], 10);
  $day = floatval($m[2]);
  $year = ($yy >= 57) ? (1900 + $yy) : (2000 + $yy);

  $dayInt = (int)floor($day);
  $frac = $day - $dayInt;

  $dt = new DateTime("$year-01-01 00:00:00", new DateTimeZone("UTC"));
  $dt->modify('+' . ($dayInt - 1) . ' days');

  $seconds = (int)round($frac * 86400);
  $dt->modify('+' . $seconds . ' seconds');

  return $dt->format('Y-m-d H:i:s');
}
