<?php
/**
 * SwapAds Client — StatisticsQueue (F238).
 *
 * Per F238 design (locked 2026-07-31):
 * - Operator-B's plugin enqueues backlink view/click events locally
 *   (no HTTP call to our server at render time)
 * - Our server's AdaptivePollCron pulls events on an adaptive schedule
 *   (default 15 min, backoff to 2 hr under load)
 * - HMAC-signed flush endpoint validates server identity
 *
 * Storage: wp_options('swapads_pending_statistics') JSON array.
 * Max 1000 entries per license (oldest pruned if exceeded).
 *
 * Naming-convention-locked (avoid tracker/ad/beacon trigger words).
 *
 * @since 1.5.3
 */

declare(strict_types=1);

namespace SwapAds\Client\Statistics;

if (!defined('ABSPATH')) { /* test mode: skip WordPress bootstrap guard */ }

final class StatisticsQueue
{
    /** Max queue size. Oldest pruned when exceeded. */
    public const MAX_QUEUE_SIZE = 1000;

    /** Option key for the queue. */
    public const OPTION_KEY = 'swapads_pending_statistics';

    /**
     * Enqueue a backlink view or click event.
     *
     * TODO-CLIENT-API-002 (2026-08-01): visitor_ip + visitor_ua are now
     * HASHES (32-char SHA-256 prefix), never raw. Caller (StatisticsFlushEndpoint)
     * is responsible for hashing the real values before calling enqueue().
     * Backwards-compat: any legacy 'visitor_ip' / 'visitor_ua' string fields
     * in the input are dropped silently.
     *
     * @param array<string, mixed> $event Must contain backlink_id, partner_id, event_type ('view' or 'click'), ts_ms.
     *                                  Optional: page_url, visitor_ip_hash, visitor_ua_hash, swapads_sid.
     */
    public static function enqueue(array $event): bool
    {
        $backlinkId = (int) ($event['backlink_id'] ?? 0);
        $partnerId = (int) ($event['partner_id'] ?? 0);
        $eventType = (string) ($event['event_type'] ?? '');
        $tsMs = (int) ($event['ts_ms'] ?? 0);
        if ($backlinkId <= 0 || $partnerId <= 0 || $tsMs <= 0) {
            return false;
        }
        if (!in_array($eventType, ['view', 'click'], true)) {
            return false;
        }

        $queue = self::load();
        // Prune oldest if at cap (LIFO drop).
        if (count($queue) >= self::MAX_QUEUE_SIZE) {
            $queue = array_slice($queue, -(self::MAX_QUEUE_SIZE - 1));
        }
        // TODO-CLIENT-API-002 (2026-08-01): strip any legacy raw IP/UA fields.
        // Accept ONLY the hash variants (visitor_ip_hash, visitor_ua_hash).
        $visitorIpHash = isset($event['visitor_ip_hash'])
            ? (string) $event['visitor_ip_hash']
            : '';
        $visitorUaHash = isset($event['visitor_ua_hash'])
            ? (string) $event['visitor_ua_hash']
            : '';
        // Defensive: if a caller still sends raw IP (e.g., legacy code
        // path), hash it here too. New callers MUST pre-hash.
        if ($visitorIpHash === '' && isset($event['visitor_ip']) && is_string($event['visitor_ip'])) {
            $visitorIpHash = substr(
                hash('sha256', $event['visitor_ip'] . wp_salt('auth')),
                0,
                32
            );
        }
        if ($visitorUaHash === '' && isset($event['visitor_ua']) && is_string($event['visitor_ua'])) {
            $visitorUaHash = substr(
                hash('sha256', $event['visitor_ua'] . wp_salt('auth')),
                0,
                32
            );
        }

        $queue[] = [
            'backlink_id'      => $backlinkId,
            'partner_id'       => $partnerId,
            'event_type'       => $eventType,
            'ts_ms'            => $tsMs,
            'page_url'         => (string) ($event['page_url'] ?? ''),
            'visitor_ip_hash'  => $visitorIpHash,
            'visitor_ua_hash'  => $visitorUaHash,
            'swapads_sid'      => isset($event['swapads_sid']) ? (string) $event['swapads_sid'] : null,
            'enqueued_at'      => time(),
        ];
        return self::save($queue);
    }

    /**
     * Drain all queued events.
     *
     * @return array<int, array<string, mixed>>
     */
    public static function drain(): array
    {
        $queue = self::load();
        self::save([]);
        return $queue;
    }

    /**
     * Drain events older than a given timestamp (used by server ACK).
     *
     * @return array<int, array<string, mixed>>
     */
    public static function drainOlderThan(int $maxEnqueuedAt): array
    {
        $queue = self::load();
        $kept = [];
        $drained = [];
        foreach ($queue as $item) {
            if ((int) ($item['enqueued_at'] ?? 0) <= $maxEnqueuedAt) {
                $drained[] = $item;
            } else {
                $kept[] = $item;
            }
        }
        self::save($kept);
        return $drained;
    }

    /**
     * Count events in queue.
     */
    public static function count(): int
    {
        return count(self::load());
    }

    /**
     * Load the queue from wp_options.
     *
     * @return array<int, array<string, mixed>>
     */
    private static function load(): array
    {
        $raw = get_option(self::OPTION_KEY, '[]');
        if (!is_string($raw)) {
            return [];
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return [];
        }
        return $decoded;
    }

    /**
     * Save the queue to wp_options.
     *
     * @param array<int, array<string, mixed>> $queue
     */
    private static function save(array $queue): bool
    {
        return (bool) update_option(self::OPTION_KEY, (string) wp_json_encode($queue), 'no');
    }

    /**
     * Generate a stable, anonymous visitor cookie token.
     *
     * Called by JS to set the swapads_sid cookie on first page-load.
     * Server uses this for cross-page dedup within operator-B's domain.
     */
    public static function mintVisitorCookie(): string
    {
        return bin2hex(random_bytes(16));
    }
}
