<?php

declare(strict_types=1);

namespace PpPhp\Core\Analytics;

/**
 * The ingest side of the unified Visitor Engine — the endpoint separate PPPHP
 * properties (marketing site, community portal, backup servers, generated apps)
 * report to, so everything lands in ONE dashboard.
 *
 * Two kinds of event:
 *   • Browser pageview/event — posted straight from a visitor's browser by
 *     pp-analytics.js. The visitor's real IP + UA reach us, so geo/device resolve
 *     normally. Authenticated only by the PUBLIC site_id (like any analytics
 *     pixel); bot-filtered and deduped by the beacon's session id.
 *   • Server report — posted by a property's PHP with the site SECRET. Trusted,
 *     so it may carry an on-behalf-of IP and API-client telemetry.
 *
 * Never throws; returns a compact {ok,code,message} the route turns into a 2xx/4xx.
 */
final class AnalyticsCollector
{
    /**
     * @param array<string,mixed> $body  decoded JSON payload
     * @param array<string,mixed> $server $_SERVER
     * @return array{ok:bool,code:int,message:string}
     */
    public static function ingest(array $body, array $server): array
    {
        $site = trim((string) ($body['site'] ?? ''));
        if ($site === '') return ['ok' => false, 'code' => 400, 'message' => 'Missing site id.'];

        $meta = AnalyticsSites::lookup($site);
        if ($meta === null) return ['ok' => false, 'code' => 404, 'message' => 'Unknown site id.'];

        $channel = $meta['channel'];
        $type = (string) ($body['type'] ?? 'pageview');
        $token = (string) ($body['token'] ?? '');
        $trusted = $token !== '' && AnalyticsSites::verifySecret($site, $token);
        $connectingIp = VisitorLog::clientIp();

        try {
            if ($type === 'api') {
                // Server-to-server API-client telemetry — must be authenticated.
                if (!$trusted) return ['ok' => false, 'code' => 401, 'message' => 'A valid site secret is required for server reports.'];
                // The reporter tells us the real client IP; fall back to its own.
                $clientIp = self::validIp((string) ($body['ip'] ?? '')) ?: $connectingIp;
                // An authenticated report may name its own API channel (license/update);
                // otherwise it inherits the site's channel, or 'api'.
                $apiChannel = in_array((string) ($body['channel'] ?? ''), VisitorLog::API_CHANNELS, true)
                    ? (string) $body['channel']
                    : (in_array($channel, VisitorLog::API_CHANNELS, true) ? $channel : 'api');
                self::withIp($clientIp, $server, static function ($srv) use ($body, $apiChannel) {
                    VisitorLog::recordApiQuery(
                        $apiChannel,
                        VisitorLog::apiAction((string) ($body['action'] ?? 'request')),
                        (int) ($body['status'] ?? 200),
                        substr((string) ($body['client'] ?? ''), 0, 190),
                        substr((string) ($body['key'] ?? ''), 0, 64)
                    );
                });
                AnalyticsSites::touch($site);
                return ['ok' => true, 'code' => 204, 'message' => ''];
            }

            // Browser pageview / event.
            if (!$meta['browser_enabled'] && !$trusted) {
                return ['ok' => false, 'code' => 403, 'message' => 'Browser reporting is disabled for this site.'];
            }
            // A trusted server report may specify the visitor IP; a browser event uses its own.
            $ip = ($trusted ? self::validIp((string) ($body['ip'] ?? '')) : '') ?: $connectingIp;
            self::withIp($ip, $server, static function ($srv) use ($body, $channel, $site, $trusted) {
                VisitorLog::recordCollected($body, $channel, $site, VisitorLog::clientIp(), $srv, $trusted);
            });
            AnalyticsSites::touch($site);
            return ['ok' => true, 'code' => 204, 'message' => ''];
        } catch (\Throwable $e) {
            return ['ok' => false, 'code' => 500, 'message' => 'Could not record the event.'];
        }
    }

    /**
     * Run $fn with REMOTE_ADDR temporarily overridden to $ip, so VisitorLog's
     * IP-derivation and geo see the intended address. Restores it after.
     */
    private static function withIp(string $ip, array $server, callable $fn): void
    {
        if ($ip === '') { $fn($server); return; }
        $saved = [$_SERVER['REMOTE_ADDR'] ?? null, $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null, $_SERVER['HTTP_CF_CONNECTING_IP'] ?? null];
        $_SERVER['REMOTE_ADDR'] = $ip;
        unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CF_CONNECTING_IP']);   // don't let a stale proxy header win
        try {
            $srv = $server;
            $srv['REMOTE_ADDR'] = $ip;
            unset($srv['HTTP_X_FORWARDED_FOR'], $srv['HTTP_CF_CONNECTING_IP']);
            $fn($srv);
        } finally {
            [$_SERVER['REMOTE_ADDR'], $xff, $cf] = [$saved[0], $saved[1], $saved[2]];
            if ($xff !== null) $_SERVER['HTTP_X_FORWARDED_FOR'] = $xff;
            if ($cf !== null) $_SERVER['HTTP_CF_CONNECTING_IP'] = $cf;
        }
    }

    private static function validIp(string $ip): string
    {
        $ip = trim($ip);
        return ($ip !== '' && filter_var($ip, FILTER_VALIDATE_IP)) ? $ip : '';
    }
}
