<?php
/**
 * request_logging.php — Lightweight request logging + IP geolocation for
 * the analytics dashboard (see analytics.php).
 */

/**
 * Get the real client IP address
 */
function real_ip(): string {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
        $ip = $_SERVER['HTTP_CLIENT_IP'];
    } elseif (isset($_SERVER['REMOTE_ADDR'])) {
        $ip = $_SERVER['REMOTE_ADDR'];
    } else {
        $ip = 'unknown';
    }
    return htmlspecialchars($ip, ENT_QUOTES, 'UTF-8');
}

/**
 * Resolve the client's real IP as a raw (non-HTML-escaped) string, honoring
 * reverse proxy headers the same way api.php / api/ip-info.php already do
 * (first entry of X-Forwarded-For, then X-Real-IP, then REMOTE_ADDR).
 * Separate from real_ip() above (which HTML-escapes for direct display and
 * doesn't take the "first" forwarded IP) so existing callers of real_ip()
 * are unaffected.
 */
function raw_client_ip(): string {
    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        return trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
    }
    if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
        return trim($_SERVER['HTTP_X_REAL_IP']);
    }
    return (string) ($_SERVER['REMOTE_ADDR'] ?? '');
}

/**
 * Best-effort country-code + lat/lon lookup for an IP, cached in
 * ip_geo_cache so we never call the upstream geolocation service more
 * than once per IP per cache window (default 7 days) — this keeps
 * request logging fast (no network call on the hot path for repeat
 * visitors) and avoids hammering the free ipwho.is service.
 *
 * Fails closed: returns '' on any lookup failure rather than blocking the
 * request the country code is being resolved for. Coordinates are
 * best-effort and may be null if the upstream lookup fails or omits them
 * (e.g. private/reserved IP ranges) — callers (analytics.php's map) must
 * handle null gracefully, not assume every IP resolves to a point.
 */
function resolveCountryCode(string $ip): string {
    global $db;

    if ($ip === '' || !isset($db)) {
        return '';
    }

    $cacheSeconds = 7 * 24 * 60 * 60;
    $now = time();

    $cached = $db->select('ip_geo_cache', '*', 'ip_address = :ip', '', [':ip' => $ip]);
    if (!empty($cached)) {
        $row = $cached[0];
        if (($now - (int) ($row['resolved_at'] ?? 0)) < $cacheSeconds) {
            return (string) ($row['country_code'] ?? '');
        }
    }

    $countryCode = '';
    $latitude = null;
    $longitude = null;
    $context = stream_context_create([
        'http' => [
            'timeout' => 3, // short timeout — this must never meaningfully delay the request being logged
            'header' => "User-Agent: IPTV-Panel-Analytics/1.0\r\n",
        ],
    ]);
    $upstream = @file_get_contents('https://ipwho.is/' . rawurlencode($ip), false, $context);
    if ($upstream !== false) {
        $data = json_decode($upstream, true);
        if (is_array($data) && !empty($data['success'])) {
            $countryCode = (string) ($data['country_code'] ?? '');
            $latitude = isset($data['latitude']) ? (float) $data['latitude'] : null;
            $longitude = isset($data['longitude']) ? (float) $data['longitude'] : null;
        }
    }

    $geoFields = [
        'country_code' => $countryCode,
        'latitude' => $latitude,
        'longitude' => $longitude,
        'resolved_at' => $now,
    ];
    if (!empty($cached)) {
        $db->update('ip_geo_cache', $geoFields, 'ip_address = :ip', [':ip' => $ip]);
    } else {
        $db->insert('ip_geo_cache', array_merge(['ip_address' => $ip], $geoFields));
    }

    return $countryCode;
}

/**
 * Records one hit against an app-facing API endpoint for the analytics
 * dashboard (analytics.php). Call this from every endpoint the
 * distributed Android app / TV clients actually call — NOT from the
 * admin-facing pages (main.php, devices.php, etc.), since this dashboard
 * measures client/app traffic, not operator page views.
 *
 * The country lookup is skipped by default (geo lookups add latency and
 * this function sits on the hot path of every app request) — pass
 * $resolveGeo = true only from lower-traffic endpoints where the extra
 * lookup is acceptable, or rely on ip_geo_cache warming up over time as
 * analytics.php's own rendering triggers lookups for IPs seen recently.
 * In this implementation, geo resolution happens lazily when the
 * analytics dashboard is viewed (see analytics.php), not on every logged
 * request, so $resolveGeo defaults to false and normally stays that way.
 */
function logRequest(string $endpoint, bool $resolveGeo = false): void {
    global $db;

    if (!isset($db)) {
        return;
    }

    $ip = raw_client_ip();
    $userAgent = (string) ($_SERVER['HTTP_USER_AGENT'] ?? '');
    $countryCode = $resolveGeo ? resolveCountryCode($ip) : '';

    $db->insert('request_log', [
        'endpoint' => $endpoint,
        'ip_address' => $ip,
        'country_code' => $countryCode,
        'user_agent' => $userAgent,
        'created_at' => time(),
    ]);

    // Opportunistic retention pruning: on ~1 in 200 inserts, delete
    // anything older than the retention window, instead of running a
    // scheduled job (this codebase has no cron runner). Keeps the table
    // bounded without adding I/O to every single request.
    if (random_int(1, 200) === 1) {
        $retentionDays = (int) ($GLOBALS['config_ini']['analytics']['retention_days'] ?? 30);
        if ($retentionDays > 0) {
            $cutoff = time() - ($retentionDays * 86400);
            $db->delete('request_log', 'created_at < :cutoff', [':cutoff' => $cutoff]);
        }
    }
}
