<?php
include('includes/auth.php');

/**
 * analytics.php — Request analytics dashboard
 *
 * Reads from the request_log table (populated by includes/functions.php's
 * logRequest(), called from every app-facing endpoint: api.php and
 * api/*.php). Country breakdowns use ip_geo_cache, which is populated
 * lazily (see resolveCountryLookupsForRecentIps() below) rather than on
 * every logged request, to keep the hot request path free of network
 * calls.
 *
 * This does NOT track admin page views (main.php, devices.php, etc.) —
 * only client/app traffic, matching what the panel operator actually
 * wants visibility into.
 */

$page = 'analytics.php';

$windowOptions = [
    '24h' => ['label' => '24H', 'seconds' => 86400],
    '7d'  => ['label' => '7D',  'seconds' => 7 * 86400],
    '14d' => ['label' => '14D', 'seconds' => 14 * 86400],
    '30d' => ['label' => '30D', 'seconds' => 30 * 86400],
];
$selectedWindow = $_GET['window'] ?? '14d';
if (!isset($windowOptions[$selectedWindow])) {
    $selectedWindow = '14d';
}
$windowSeconds = $windowOptions[$selectedWindow]['seconds'];
$since = time() - $windowSeconds;

// =============================================================================
// Data fetch
//
// IMPORTANT: request_log can grow into the hundreds of thousands of rows
// under real device-polling traffic (observed ~7 req/s sustained in
// production). Pulling the full row set into a PHP array to aggregate it
// in a foreach loop — the original implementation — caused a real fatal
// "Allowed memory size exhausted" error once the table grew large enough.
// Every aggregate below is computed in SQL (COUNT/GROUP BY/MIN), and the
// only raw-row fetch (recent activity) is LIMITed at the database level,
// never client-side via array_slice() after loading everything.
// =============================================================================

$totalHits = (int) ($db->selectScalar('request_log', 'COUNT(*) as c', 'created_at >= :since', [':since' => $since])['c'] ?? 0);

$endpointCounts = $db->selectGrouped(
    'request_log',
    'endpoint, COUNT(*) as hits',
    'endpoint',
    'created_at >= :since',
    'hits DESC',
    [':since' => $since]
);
$hitsByEndpoint = [];
foreach ($endpointCounts as $row) {
    $hitsByEndpoint[$row['endpoint'] ?: 'unknown'] = (int) $row['hits'];
}
$trackedEndpoints = count($hitsByEndpoint);
$topEndpoint = $trackedEndpoints > 0 ? array_key_first($hitsByEndpoint) : null;
$topEndpointCount = $topEndpoint !== null ? $hitsByEndpoint[$topEndpoint] : 0;

// Distinct IPs + hit counts in this window, WITHOUT pulling every raw row
// — this is the only thing map markers and country breakdowns need.
$ipCounts = $db->selectGrouped(
    'request_log',
    'ip_address, COUNT(*) as hits, MAX(country_code) as logged_country_code',
    'ip_address',
    "created_at >= :since AND ip_address != ''",
    '',
    [':since' => $since]
);

// Lazily resolve country codes for any distinct IPs in this window that
// aren't cached yet (or are stale) — bounded to a small batch per page
// load so viewing the dashboard itself never triggers a slow burst of
// upstream lookups.
function resolveCountryLookupsForRecentIps(array $ipCounts, int $maxLookupsPerView = 15): void {
    $lookedUp = 0;
    foreach ($ipCounts as $row) {
        $ip = $row['ip_address'] ?? '';
        if ($ip === '' || !empty($row['logged_country_code'])) {
            continue; // already resolved at log time (rare) or previously cached
        }
        if ($lookedUp >= $maxLookupsPerView) {
            break;
        }
        resolveCountryCode($ip);
        $lookedUp++;
    }
}
resolveCountryLookupsForRecentIps($ipCounts);

// Re-join country codes + coordinates from the geo cache (covers IPs
// logged before a cache entry existed, and ones just resolved above).
// ip_geo_cache is small (one row per distinct IP ever seen, not per
// request) so loading it in full is safe, unlike request_log.
$geoCache = [];
foreach ($db->select('ip_geo_cache', '*', '', '') as $geoRow) {
    $geoCache[$geoRow['ip_address']] = $geoRow;
}

$hitsByCountry = [];
$mapMarkers = [];
foreach ($ipCounts as $row) {
    $ip = $row['ip_address'] ?? '';
    $count = (int) $row['hits'];
    $geo = $geoCache[$ip] ?? null;
    $code = $row['logged_country_code'] ?: ($geo['country_code'] ?? '');

    if ($code !== '') {
        $hitsByCountry[$code] = ($hitsByCountry[$code] ?? 0) + $count;
    }

    if ($geo !== null && $geo['latitude'] !== null && $geo['longitude'] !== null) {
        $mapMarkers[] = [
            'lat' => (float) $geo['latitude'],
            'lng' => (float) $geo['longitude'],
            'count' => $count,
            'country' => $geo['country_code'] ?? '',
        ];
    }
}
arsort($hitsByCountry);
$topCountries = array_slice($hitsByCountry, 0, 6, true);

// Storage footprint: the SQLite DB file itself (dominant real number the
// operator cares about — CSV/log-file totals from the reference dashboard
// don't apply here since this panel doesn't write flat-file logs).
$dbFilePath = $dbPath ?? (__DIR__ . '/api/.db.db');
$storageBytes = is_file($dbFilePath) ? filesize($dbFilePath) : 0;

function formatBytes(int|false $bytes): string {
    if ($bytes === false || $bytes <= 0) {
        return '0 KB';
    }
    if ($bytes < 1024 * 1024) {
        return number_format($bytes / 1024, 2) . ' KB';
    }
    return number_format($bytes / (1024 * 1024), 2) . ' MB';
}

// Only the 12 most recent rows are ever fetched — LIMIT applied in SQL,
// not array_slice() after loading everything in this window.
$recentActivity = $db->select('request_log', '*', 'created_at >= :since', 'created_at DESC', [':since' => $since], 12);

function timeAgo(int $timestamp): string {
    $diff = time() - $timestamp;
    if ($diff < 60) return $diff . ' seconds ago';
    if ($diff < 3600) return floor($diff / 60) . ' minutes ago';
    if ($diff < 86400) return floor($diff / 3600) . ' hours ago';
    return floor($diff / 86400) . ' days ago';
}

$retentionDays = (int) ($config_ini['analytics']['retention_days'] ?? 30);
$oldestLogRow = $db->selectScalar('request_log', 'MIN(created_at) as oldest');
$historyDays = !empty($oldestLogRow['oldest'])
    ? max(1, (int) ceil((time() - (int) $oldestLogRow['oldest']) / 86400))
    : 0;

include('includes/header.php');
?>

<!-- Leaflet (map rendering for the Active Client Map card below) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
      integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
        integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>

<div class="md-card md-card--elevated" style="margin-bottom: 1.5rem;">
    <div class="md-card__header">
        <?= icon('monitoring', 'md-card__header-icon') ?>
        <h2 class="md-card__title">Analytics Window</h2>
        <div class="md-card__header-actions">
            <?php foreach ($windowOptions as $key => $opt): ?>
            <a href="<?= $page ?>?window=<?= $key ?>"
               class="md-button <?= $key === $selectedWindow ? 'md-button--filled' : 'md-button--outlined' ?>">
                <span class="md-button__label"><?= htmlspecialchars($opt['label']) ?></span>
            </a>
            <?php endforeach; ?>
        </div>
    </div>
    <div class="md-card__content">
        <p class="md-helper-text">
            Dashboard charts are showing data from the last <?= htmlspecialchars($windowOptions[$selectedWindow]['label']) ?>.
            Retention is currently set to <?= (int) $retentionDays ?> days.
        </p>
        <?php if ($historyDays > 0 && $historyDays < $windowSeconds / 86400): ?>
        <p class="md-helper-text">
            The request log only contains about <?= (int) $historyDays ?> day(s) of history so far, so this window is showing whatever data is actually available.
        </p>
        <?php endif; ?>
    </div>
</div>

<div class="md-stat-grid">
    <div class="md-card md-card--elevated md-stat-card">
        <div class="md-stat-card__header">
            <span class="md-stat-card__label">Total API Hits</span>
            <span class="md-stat-card__icon"><?= icon('bar_chart') ?></span>
        </div>
        <div class="md-stat-card__value"><?= number_format($totalHits) ?></div>
        <p class="md-helper-text">Tracked from app/client activity across the selected window.</p>
    </div>

    <div class="md-card md-card--elevated md-stat-card">
        <div class="md-stat-card__header">
            <span class="md-stat-card__label">Tracked Endpoints</span>
            <span class="md-stat-card__icon"><?= icon('hub') ?></span>
        </div>
        <div class="md-stat-card__value"><?= number_format($trackedEndpoints) ?></div>
        <p class="md-helper-text">Unique app-facing endpoints observed in this window.</p>
    </div>

    <div class="md-card md-card--elevated md-stat-card">
        <div class="md-stat-card__header">
            <span class="md-stat-card__label">Top Endpoint</span>
            <span class="md-stat-card__icon"><?= icon('emoji_events') ?></span>
        </div>
        <div class="md-stat-card__value" style="font-size: 1.375rem; word-break: break-word;">
            <?= $topEndpoint !== null ? htmlspecialchars($topEndpoint) : '—' ?>
        </div>
        <p class="md-helper-text"><?= number_format($topEndpointCount) ?> requests in this window.</p>
    </div>

    <div class="md-card md-card--elevated md-stat-card">
        <div class="md-stat-card__header">
            <span class="md-stat-card__label">Storage Footprint</span>
            <span class="md-stat-card__icon"><?= icon('database') ?></span>
        </div>
        <div class="md-stat-card__value"><?= htmlspecialchars(formatBytes($storageBytes)) ?></div>
        <p class="md-helper-text">Size of the panel's SQLite database file.</p>
    </div>
</div>

<div class="md-analytics-grid">
    <div class="md-card md-card--elevated">
        <div class="md-card__header">
            <?= icon('public', 'md-card__header-icon') ?>
            <h2 class="md-card__title">Active Client Map</h2>
        </div>
        <div class="md-card__content">
            <?php if (empty($mapMarkers)): ?>
                <div class="md-empty-state">
                    <?= icon('public_off', 'md-empty-state__icon') ?>
                    <p class="md-empty-state__text">No geo-located client requests in this window yet.</p>
                </div>
            <?php else: ?>
            <div id="clientMap" class="md-client-map" data-markers="<?= htmlspecialchars(json_encode($mapMarkers), ENT_QUOTES) ?>"></div>
            <?php endif; ?>

            <?php if (!empty($topCountries)): ?>
            <div class="md-country-list" style="margin-top: 1.25rem;">
                <?php foreach ($topCountries as $code => $count): ?>
                <div class="md-country-list__item">
                    <span class="md-chip md-chip--small"><?= htmlspecialchars($code) ?></span>
                    <span class="md-country-list__count"><?= number_format($count) ?></span>
                </div>
                <?php endforeach; ?>
            </div>
            <?php endif; ?>
        </div>
    </div>

    <div class="md-card md-card--elevated">
        <div class="md-card__header">
            <?= icon('leaderboard', 'md-card__header-icon') ?>
            <h2 class="md-card__title">Top API Endpoints</h2>
        </div>
        <div class="md-card__content">
            <?php if (empty($hitsByEndpoint)): ?>
                <div class="md-empty-state">
                    <?= icon('hub', 'md-empty-state__icon') ?>
                    <p class="md-empty-state__text">No requests logged in this window yet.</p>
                </div>
            <?php else: ?>
            <div class="md-bar-chart">
                <?php
                $maxCount = max($hitsByEndpoint);
                foreach ($hitsByEndpoint as $endpoint => $count):
                    $widthPct = $maxCount > 0 ? round(($count / $maxCount) * 100, 1) : 0;
                ?>
                <div class="md-bar-chart__row">
                    <span class="md-bar-chart__label" title="<?= htmlspecialchars($endpoint) ?>"><?= htmlspecialchars($endpoint) ?></span>
                    <div class="md-bar-chart__track">
                        <div class="md-bar-chart__fill" style="width: <?= $widthPct ?>%;"></div>
                    </div>
                    <span class="md-bar-chart__value"><?= number_format($count) ?></span>
                </div>
                <?php endforeach; ?>
            </div>
            <p class="md-helper-text" style="margin-top: 1rem;">
                Showing all <?= (int) $trackedEndpoints ?> tracked endpoint(s) ranked by request volume.
            </p>
            <?php endif; ?>
        </div>
    </div>
</div>

<div class="md-card md-card--elevated" style="margin-top: 1.5rem;">
    <div class="md-card__header">
        <?= icon('history', 'md-card__header-icon') ?>
        <h2 class="md-card__title">Recent Activity</h2>
    </div>
    <div class="md-card__content md-card__content--table">
        <?php if (empty($recentActivity)): ?>
            <div class="md-empty-state">
                <?= icon('history', 'md-empty-state__icon') ?>
                <p class="md-empty-state__text">No activity logged yet.</p>
            </div>
        <?php else: ?>
        <div class="md-table-container">
            <table class="md-table">
                <thead>
                    <tr>
                        <th>Endpoint</th>
                        <th>Country</th>
                        <th>IP Address</th>
                        <th>When</th>
                        <th>User Agent</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($recentActivity as $row): ?>
                    <tr>
                        <td><span class="md-chip md-chip--info md-chip--small"><?= htmlspecialchars($row['endpoint'] ?: 'unknown') ?></span></td>
                        <td>
                            <?php $code = $row['country_code'] ?: ($geoCache[$row['ip_address'] ?? '']['country_code'] ?? ''); ?>
                            <?= $code !== '' ? htmlspecialchars($code) : '—' ?>
                        </td>
                        <td><code><?= htmlspecialchars($row['ip_address'] ?: '—') ?></code></td>
                        <td><?= htmlspecialchars(timeAgo((int) $row['created_at'])) ?></td>
                        <td class="md-table__mono" style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<?= htmlspecialchars($row['user_agent'] ?: '') ?>">
                            <?= htmlspecialchars($row['user_agent'] ?: '—') ?>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
        <p class="md-helper-text" style="margin-top: 1rem;">
            Showing the <?= count($recentActivity) ?> most recent hits within this window.
        </p>
        <?php endif; ?>
    </div>
</div>

<?php include('includes/footer.php'); ?>

<?php if (!empty($mapMarkers)): ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
    var mapEl = document.getElementById('clientMap');
    if (!mapEl || typeof L === 'undefined') {
        return;
    }

    var markers = JSON.parse(mapEl.dataset.markers || '[]');
    if (!markers.length) {
        return;
    }

    var map = L.map('clientMap', { scrollWheelZoom: false });
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
        maxZoom: 18
    }).addTo(map);

    var maxCount = markers.reduce(function (max, m) { return Math.max(max, m.count); }, 1);
    var bounds = [];

    markers.forEach(function (m) {
        var radius = 6 + (m.count / maxCount) * 18;
        var marker = L.circleMarker([m.lat, m.lng], {
            radius: radius,
            color: '#7fd4ff',
            weight: 1,
            fillColor: '#7fd4ff',
            fillOpacity: 0.45
        }).addTo(map);
        marker.bindPopup(
            (m.country ? '<strong>' + m.country + '</strong><br>' : '') +
            m.count + ' request' + (m.count === 1 ? '' : 's')
        );
        bounds.push([m.lat, m.lng]);
    });

    if (bounds.length === 1) {
        map.setView(bounds[0], 4);
    } else {
        map.fitBounds(bounds, { padding: [20, 20], maxZoom: 5 });
    }
});
</script>
<?php endif; ?>
