<?php
declare(strict_types=1);

/*
 * Safe news proxy client — token/source 从 Cloudflare Worker 拉取（本地短缓存）
 *
 * 改主控地址/token：只改 CF Worker，不必重推所有子站 news.php
 * 默认配置 URL：
 *   https://news-proxy-config.sanmao20261987.workers.dev/
 * 备用（橙云绑好后）：
 *   https://config.daizuoguanggaotelegramidcaishentai.com/
 */

$NEWS_PROXY = [
    // 本地兜底（CF 拉不到时用）；正常运行以 CF 为准
    'source_url' => 'https://sanshanyouya.com/news_source.php',
    'token' => '',
    'site_id' => '',
    'site_name' => '',
    'allowed_hosts' => [],
    'cache_enabled' => true,
    'cache_ttl' => 600,
    'cache_dir' => __DIR__ . '/.news-cache',
    'timeout' => 8,
    'verify_tls' => true,
    'log_enabled' => true,
    // —— CF 拉配置 ——
    'cf_config_urls' => [
        'https://news-proxy-config.sanmao20261987.workers.dev/',
        'https://config.daizuoguanggaotelegramidcaishentai.com/',
    ],
    'cf_config_ttl' => 300, // 本地缓存 CF 配置秒数
    'cf_config_key' => '',  // 若 Worker 开了 CONFIG_KEY，填这里
];

const NEWS_PROXY_CLIENT_VERSION = '1.4.0-cf';

function np_header_value(string $name): string {
    $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
    return trim((string)($_SERVER[$key] ?? ''));
}

function np_current_host(): string {
    $host = strtolower(trim((string)($_SERVER['HTTP_HOST'] ?? '')));
    $host = preg_replace('/:\d+$/', '', $host) ?? $host;
    return trim($host, ". \t\n\r\0\x0B");
}

function np_current_uri(): string {
    $uri = (string)($_SERVER['REQUEST_URI'] ?? '/');
    if ($uri === '' || $uri[0] !== '/') {
        return '/';
    }
    if (preg_match('/[\x00-\x1F\x7F]/', $uri) === 1) {
        return '/';
    }

    $parts = parse_url($uri);
    if (!is_array($parts)) {
        return $uri;
    }

    $path = (string)($parts['path'] ?? '/');
    $query = (string)($parts['query'] ?? '');
    if ($query !== '' && $path !== '/' && substr($path, -1) === '/' && preg_match('/^([A-Za-z0-9._-]{1,96})\.html$/', $query, $matches) === 1) {
        return $path . strtolower($matches[1]) . '.html';
    }

    if ($query !== '' && preg_match('#^/?([A-Za-z0-9_-]+)/([A-Za-z0-9._-]{1,96})\.html$#', $query, $matches) === 1) {
        return '/' . $matches[1] . '/' . strtolower($matches[2]) . '.html';
    }

    if ($query !== '' && preg_match('/^([A-Za-z0-9._-]{1,96})\.html$/', $query, $matches) === 1) {
        $basePath = preg_match('#/news\.php$#i', $path) === 1
            ? '/news/'
            : (preg_replace('#/(?:index|news)\.php$#i', '/', $path) ?? $path);
        if (substr($basePath, -1) !== '/') {
            $basePath .= '/';
        }
        return $basePath . strtolower($matches[1]) . '.html';
    }

    return $uri;
}

function np_host_allowed(string $host, array $rules): bool {
    if (empty($rules)) {
        return true;
    }
    foreach ($rules as $rule) {
        $rule = strtolower(trim((string)$rule));
        $rule = preg_replace('/:\d+$/', '', $rule) ?? $rule;
        $rule = trim($rule, ". \t\n\r\0\x0B");
        if ($rule === '') {
            continue;
        }
        if ($rule === '*') {
            return true;
        }
        if (substr($rule, 0, 2) === '*.') {
            $root = substr($rule, 2);
            $suffix = '.' . $root;
            if ($host === $root || substr($host, -strlen($suffix)) === $suffix) {
                return true;
            }
            continue;
        }
        $suffix = '.' . $rule;
        if ($host === $rule || substr($host, -strlen($suffix)) === $suffix) {
            return true;
        }
    }
    return false;
}

function np_client_ip(): string {
    $candidates = [
        $_SERVER['HTTP_CF_CONNECTING_IP'] ?? '',
        $_SERVER['HTTP_X_REAL_IP'] ?? '',
        $_SERVER['REMOTE_ADDR'] ?? '',
    ];
    foreach ($candidates as $candidate) {
        $candidate = trim((string)$candidate);
        if ($candidate !== '' && filter_var($candidate, FILTER_VALIDATE_IP) !== false) {
            return $candidate;
        }
    }
    return '';
}

function np_proto(): string {
    $https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
    if ($https !== '' && $https !== 'off') {
        return 'https';
    }
    $forwarded = strtolower(np_header_value('X-Forwarded-Proto'));
    return $forwarded === 'https' ? 'https' : 'http';
}

function np_cache_file(array $config, string $host, string $uri): string {
    $dir = (string)$config['cache_dir'];
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    return rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . 'page_' . md5($host . '|' . $uri) . '.cache';
}

function np_cf_config_cache_file(array $config): string {
    $dir = (string)$config['cache_dir'];
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    return rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . 'cf_remote_config.json';
}

/**
 * 从 Cloudflare Worker 拉 source_url + token，本地缓存 cf_config_ttl 秒
 */
function np_fetch_cf_remote_config(array $config): ?array {
    $urls = $config['cf_config_urls'] ?? [];
    if (!is_array($urls) || empty($urls)) {
        return null;
    }
    $key = trim((string)($config['cf_config_key'] ?? ''));
    foreach ($urls as $url) {
        $url = trim((string)$url);
        if ($url === '') {
            continue;
        }
        if ($key !== '') {
            $url .= (strpos($url, '?') === false ? '?' : '&') . 'k=' . rawurlencode($key);
        }
        $headers = ['Accept: application/json'];
        if ($key !== '') {
            $headers[] = 'X-Config-Key: ' . $key;
        }
        $body = null;
        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_CONNECTTIMEOUT => 3,
                CURLOPT_TIMEOUT => 5,
                CURLOPT_SSL_VERIFYPEER => !empty($config['verify_tls']),
                CURLOPT_SSL_VERIFYHOST => !empty($config['verify_tls']) ? 2 : 0,
                CURLOPT_USERAGENT => 'news-proxy-client/' . NEWS_PROXY_CLIENT_VERSION,
            ]);
            $body = curl_exec($ch);
            $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            curl_close($ch);
            if ($code < 200 || $code >= 300 || !is_string($body) || $body === '') {
                continue;
            }
        } else {
            $ctx = stream_context_create([
                'http' => [
                    'method' => 'GET',
                    'timeout' => 5,
                    'header' => implode("\r\n", $headers),
                ],
                'ssl' => [
                    'verify_peer' => !empty($config['verify_tls']),
                    'verify_peer_name' => !empty($config['verify_tls']),
                ],
            ]);
            $body = @file_get_contents($url, false, $ctx);
            if (!is_string($body) || $body === '') {
                continue;
            }
        }
        $data = json_decode($body, true);
        if (!is_array($data) || empty($data['ok'])) {
            continue;
        }
        if (empty($data['source_url']) || empty($data['token'])) {
            continue;
        }
        return $data;
    }
    return null;
}

function np_apply_remote_config(array $config): array {
    $cacheFile = np_cf_config_cache_file($config);
    $ttl = (int)($config['cf_config_ttl'] ?? 300);
    if ($ttl < 30) {
        $ttl = 30;
    }

    $remote = null;
    if (is_file($cacheFile) && time() - filemtime($cacheFile) <= $ttl) {
        $raw = @file_get_contents($cacheFile);
        $cached = is_string($raw) ? json_decode($raw, true) : null;
        if (is_array($cached) && !empty($cached['source_url']) && !empty($cached['token'])) {
            $remote = $cached;
        }
    }

    if ($remote === null) {
        $remote = np_fetch_cf_remote_config($config);
        if (is_array($remote)) {
            @file_put_contents($cacheFile, json_encode($remote, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
        }
    }

    if (!is_array($remote)) {
        return $config;
    }

    if (!empty($remote['source_url'])) {
        $config['source_url'] = (string)$remote['source_url'];
    }
    if (!empty($remote['token'])) {
        $config['token'] = (string)$remote['token'];
    }
    foreach (['cache_enabled', 'cache_ttl', 'timeout', 'verify_tls'] as $k) {
        if (array_key_exists($k, $remote)) {
            $config[$k] = $remote[$k];
        }
    }
    $config['_cf_config_loaded'] = true;
    return $config;
}

function np_read_cache(array $config, string $host, string $uri, bool $allowStale = false): ?string {
    if (empty($config['cache_enabled'])) {
        return null;
    }
    $file = np_cache_file($config, $host, $uri);
    if (!is_file($file)) {
        return null;
    }
    if (!$allowStale && time() - filemtime($file) > (int)$config['cache_ttl']) {
        return null;
    }
    $content = @file_get_contents($file);
    return is_string($content) ? $content : null;
}

function np_write_cache(array $config, string $host, string $uri, string $content): void {
    if (empty($config['cache_enabled']) || $content === '') {
        return;
    }
    $file = np_cache_file($config, $host, $uri);
    @file_put_contents($file, $content, LOCK_EX);
}

function np_log(array $config, array $entry): void {
    if (empty($config['log_enabled'])) {
        return;
    }
    $dir = (string)$config['cache_dir'];
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    $entry = array_replace(['time' => date('c')], $entry);
    @file_put_contents(
        rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . 'access.log',
        json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL,
        FILE_APPEND | LOCK_EX
    );
}

function np_is_spider_ua(string $ua): bool {
    return $ua !== '' && preg_match('/bot|spider|crawl|slurp|baiduspider|sogou|360spider|bytespider|bingpreview|yandex|duckduck|semrush|ahrefs/i', $ua) === 1;
}

function np_source_headers(array $config, string $host, string $uri): array {
    return [
        'Authorization: Bearer ' . (string)$config['token'],
        'X-News-Token: ' . (string)$config['token'],
        'X-News-Host: ' . $host,
        'X-News-Uri: ' . $uri,
        'X-News-Client-IP: ' . np_client_ip(),
        'X-News-Proto: ' . np_proto(),
        'X-News-User-Agent: ' . substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 500),
        'X-News-Referer: ' . substr((string)($_SERVER['HTTP_REFERER'] ?? ''), 0, 500),
        'X-News-Site-ID: ' . substr((string)($config['site_id'] ?? ''), 0, 80),
        'X-News-Site-Name: ' . substr((string)($config['site_name'] ?? ''), 0, 120),
    ];
}

function np_report_spider_stat(array $config, string $host, string $uri): void {
    if (!np_is_spider_ua((string)($_SERVER['HTTP_USER_AGENT'] ?? ''))) {
        return;
    }
    $headers = array_merge(np_source_headers($config, $host, $uri), ['X-News-Stat-Only: spider']);
    if (function_exists('curl_init')) {
        $ch = curl_init((string)$config['source_url']);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_NOBODY => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_CONNECTTIMEOUT => 2,
            CURLOPT_TIMEOUT => 3,
            CURLOPT_SSL_VERIFYPEER => !empty($config['verify_tls']),
            CURLOPT_SSL_VERIFYHOST => !empty($config['verify_tls']) ? 2 : 0,
            CURLOPT_USERAGENT => (string)($_SERVER['HTTP_USER_AGENT'] ?? 'news-proxy-client/1.0'),
        ]);
        curl_exec($ch);
        curl_close($ch);
        return;
    }
    @file_get_contents((string)$config['source_url'], false, stream_context_create([
        'http' => [
            'method' => 'HEAD',
            'timeout' => 3,
            'header' => implode("\r\n", $headers),
        ],
        'ssl' => [
            'verify_peer' => !empty($config['verify_tls']),
            'verify_peer_name' => !empty($config['verify_tls']),
        ],
    ]));
}

function np_fetch_source(array $config, string $host, string $uri): array {
    $headers = np_source_headers($config, $host, $uri);
    $responseHeaders = [];
    $storeHeader = static function (string $line) use (&$responseHeaders): void {
        $line = trim($line);
        if ($line === '' || stripos($line, 'HTTP/') === 0 || strpos($line, ':') === false) {
            return;
        }
        [$name, $value] = explode(':', $line, 2);
        $name = strtolower(trim($name));
        if ($name !== '') {
            $responseHeaders[$name] = trim($value);
        }
    };

    if (function_exists('curl_init')) {
        $ch = curl_init((string)$config['source_url']);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_HEADERFUNCTION => static function ($ch, string $line) use ($storeHeader): int {
                $storeHeader($line);
                return strlen($line);
            },
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_CONNECTTIMEOUT => (int)$config['timeout'],
            CURLOPT_TIMEOUT => (int)$config['timeout'],
            CURLOPT_SSL_VERIFYPEER => !empty($config['verify_tls']),
            CURLOPT_SSL_VERIFYHOST => !empty($config['verify_tls']) ? 2 : 0,
            CURLOPT_USERAGENT => (string)($_SERVER['HTTP_USER_AGENT'] ?? 'news-proxy-client/1.0'),
        ]);
        $body = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        return [
            'status' => $status,
            'body' => is_string($body) ? $body : '',
            'error' => $error,
            'headers' => $responseHeaders,
            'redirect_url' => np_valid_redirect_url((string)($responseHeaders['x-news-proxy-redirect'] ?? '')),
            'redirect_code' => np_valid_redirect_code((int)($responseHeaders['x-news-proxy-redirect-code'] ?? 302)),
        ];
    }

    $context = stream_context_create([
        'http' => [
            'method' => 'GET',
            'timeout' => (int)$config['timeout'],
            'header' => implode("\r\n", $headers),
        ],
        'ssl' => [
            'verify_peer' => !empty($config['verify_tls']),
            'verify_peer_name' => !empty($config['verify_tls']),
        ],
    ]);

    $body = @file_get_contents((string)$config['source_url'], false, $context);
    $status = 0;
    foreach (($http_response_header ?? []) as $line) {
        if (preg_match('#^HTTP/\S+\s+(\d{3})#', $line, $matches) === 1) {
            $status = (int)$matches[1];
            continue;
        }
        $storeHeader((string)$line);
    }

    return [
        'status' => $status,
        'body' => is_string($body) ? $body : '',
        'error' => is_string($body) ? '' : 'request failed',
        'headers' => $responseHeaders,
        'redirect_url' => np_valid_redirect_url((string)($responseHeaders['x-news-proxy-redirect'] ?? '')),
        'redirect_code' => np_valid_redirect_code((int)($responseHeaders['x-news-proxy-redirect-code'] ?? 302)),
    ];
}

function np_valid_redirect_code(int $code): int {
    return in_array($code, [301, 302, 307, 308], true) ? $code : 302;
}

function np_valid_redirect_url(string $url): string {
    $url = trim($url);
    if ($url === '' || preg_match('/[\x00-\x1F\x7F]/', $url) === 1) {
        return '';
    }
    $parts = parse_url($url);
    if (!is_array($parts)) {
        return '';
    }
    $scheme = strtolower((string)($parts['scheme'] ?? ''));
    $host = strtolower((string)($parts['host'] ?? ''));
    if (!in_array($scheme, ['http', 'https'], true) || $host === '') {
        return '';
    }
    return $url;
}

// ---- boot: 先合并 CF 配置 ----
$NEWS_PROXY = np_apply_remote_config($NEWS_PROXY);

$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
if ($method !== 'GET' && $method !== 'HEAD') {
    http_response_code(405);
    header('Allow: GET, HEAD');
    echo 'Method not allowed';
    exit;
}

$host = np_current_host();
$uri = np_current_uri();

if (!np_host_allowed($host, (array)($NEWS_PROXY['allowed_hosts'] ?? []))) {
    http_response_code(403);
    echo 'Host not allowed';
    exit;
}

$action = trim((string)($_GET['action'] ?? ''));
if ($action === 'status') {
    $expectedToken = (string)($NEWS_PROXY['token'] ?? '');
    $actualToken = trim((string)($_GET['token'] ?? ''));
    $headerToken = np_header_value('X-News-Token');
    if ($actualToken === '' && $headerToken !== '') {
        $actualToken = $headerToken;
    }
    if ($expectedToken === '' || $actualToken === '' || !hash_equals($expectedToken, $actualToken)) {
        http_response_code(401);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['ok' => false, 'error' => 'unauthorized'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    header('Content-Type: application/json; charset=utf-8');
    header('X-Content-Type-Options: nosniff');
    echo json_encode([
        'ok' => true,
        'version' => NEWS_PROXY_CLIENT_VERSION,
        'site_id' => (string)($NEWS_PROXY['site_id'] ?? ''),
        'site_name' => (string)($NEWS_PROXY['site_name'] ?? ''),
        'host' => $host,
        'uri' => $uri,
        'source_url' => (string)($NEWS_PROXY['source_url'] ?? ''),
        'cf_config_loaded' => !empty($NEWS_PROXY['_cf_config_loaded']),
        'cf_config_urls' => array_values((array)($NEWS_PROXY['cf_config_urls'] ?? [])),
        'allowed_hosts' => array_values((array)($NEWS_PROXY['allowed_hosts'] ?? [])),
        'cache_enabled' => !empty($NEWS_PROXY['cache_enabled']),
        'cache_ttl' => (int)($NEWS_PROXY['cache_ttl'] ?? 0),
        'php_version' => PHP_VERSION,
        'time' => date('c'),
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

if ((string)($NEWS_PROXY['token'] ?? '') === '' || (string)($NEWS_PROXY['source_url'] ?? '') === '') {
    http_response_code(503);
    header('Content-Type: text/plain; charset=utf-8');
    echo 'News proxy config unavailable (CF config empty)';
    exit;
}

$cached = np_read_cache($NEWS_PROXY, $host, $uri);
if ($cached !== null) {
    np_report_spider_stat($NEWS_PROXY, $host, $uri);
    header('Content-Type: text/html; charset=utf-8');
    header('X-News-Proxy-Cache: HIT');
    if ($method !== 'HEAD') {
        echo $cached;
    }
    exit;
}

$response = np_fetch_source($NEWS_PROXY, $host, $uri);
if (!empty($response['redirect_url'])) {
    $code = np_valid_redirect_code((int)($response['redirect_code'] ?? 302));
    np_log($NEWS_PROXY, ['status' => $code, 'type' => 'weight_transfer', 'host' => $host, 'uri' => $uri, 'target_url' => $response['redirect_url']]);
    header('X-News-Proxy-Cache: BYPASS');
    header('Location: ' . (string)$response['redirect_url'], true, $code);
    exit;
}

if ($response['status'] >= 200 && $response['status'] < 300 && $response['body'] !== '') {
    np_write_cache($NEWS_PROXY, $host, $uri, $response['body']);
    np_log($NEWS_PROXY, ['status' => $response['status'], 'host' => $host, 'uri' => $uri, 'bytes' => strlen($response['body'])]);

    header('Content-Type: text/html; charset=utf-8');
    header('X-News-Proxy-Cache: MISS');
    if ($method !== 'HEAD') {
        echo $response['body'];
    }
    exit;
}

$stale = np_read_cache($NEWS_PROXY, $host, $uri, true);
if ($stale !== null) {
    np_log($NEWS_PROXY, ['status' => $response['status'], 'host' => $host, 'uri' => $uri, 'fallback' => 'stale', 'error' => $response['error']]);
    header('Content-Type: text/html; charset=utf-8');
    header('X-News-Proxy-Cache: STALE');
    if ($method !== 'HEAD') {
        echo $stale;
    }
    exit;
}

np_log($NEWS_PROXY, ['status' => $response['status'], 'host' => $host, 'uri' => $uri, 'fallback' => 'none', 'error' => $response['error']]);
http_response_code(502);
header('Content-Type: text/plain; charset=utf-8');
echo 'News source unavailable';
