<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress\Events;

use Numinix\SeekmodoWordPress\Gateway\ShopperContext;
use Numinix\SeekmodoWordPress\Rest\Routes;

/**
 * REST + JS-side click beacon.
 *
 * Path 1: Browser beacon (preferred).
 *   The bundled `seekmodo-beacon.js` listens for clicks on rows that
 *   carry `data-seekmodo-id` + `data-seekmodo-pos`. On click it
 *   POSTs to `/wp-json/seekmodo/v1/click` with
 *   `{keyword, object_id, position, surface}` via
 *   `navigator.sendBeacon()` (zero added latency). Theme can opt in
 *   per template by enqueuing the script; the plugin auto-enqueues
 *   on `is_search()` templates.
 *
 * Path 2: PHP mirror.
 *   {@see numinix_seekmodo_mirror_click()} (and its typeahead alias)
 *   write directly to the events queue. Used by custom themes that
 *   don't want the JS beacon, or by server-side click tracking
 *   middleware.
 *
 * Either path lands a row on the local events queue. The actual
 * gateway forwarding happens on the EventsFlushCron tick (every
 * 60s), which keeps the visitor's request fast even if the gateway
 * is slow / down.
 */
final class ClickBeacon
{
    public static function register(): void
    {
        if (!function_exists('add_action')) {
            return;
        }
        add_action('rest_api_init',       [self::class, 'registerRoutes']);
        add_action('wp_enqueue_scripts',  [self::class, 'enqueue']);
        // SearchSwap runs during the main query — after wp_enqueue_scripts —
        // so refresh SEEKMODO_BEACON.search_event_id in the footer once known.
        add_action('wp_footer',           [self::class, 'printSearchEventId'], 5);
    }

    /**
     * After SearchSwap stores meta.search_event_id, expose it to the beacon.
     */
    public static function printSearchEventId(): void
    {
        if (!class_exists(\Numinix\SeekmodoWordPress\Commerce\SearchSwap::class)) {
            return;
        }
        $seid = (int) \Numinix\SeekmodoWordPress\Commerce\SearchSwap::lastSearchEventId();
        if ($seid <= 0) {
            return;
        }
        echo '<script>window.SEEKMODO_BEACON=window.SEEKMODO_BEACON||{};'
            . 'window.SEEKMODO_BEACON.search_event_id=' . wp_json_encode($seid) . ';</script>' . "\n";
    }

    public static function registerRoutes(): void
    {
        if (!function_exists('register_rest_route')) {
            return;
        }
        register_rest_route(Routes::NAMESPACE_NAME, '/click', [
            'methods'             => 'POST',
            'callback'            => [self::class, 'handle'],
            'permission_callback' => '__return_true',
            'args'                => [
                'keyword' => [
                    'required'          => true,
                    'sanitize_callback' => 'sanitize_text_field',
                ],
                'object_id' => [
                    'required'          => true,
                    'sanitize_callback' => 'absint',
                ],
                'position' => [
                    'required'          => false,
                    'sanitize_callback' => 'absint',
                    'default'           => 1,
                ],
                'surface' => [
                    'required'          => false,
                    'sanitize_callback' => 'sanitize_key',
                    'default'           => 'serp',
                ],
                'search_event_id' => [
                    'required'          => false,
                    'sanitize_callback' => 'absint',
                    'default'           => 0,
                ],
            ],
        ]);
    }

    public static function enqueue(): void
    {
        if (!function_exists('wp_enqueue_script')) {
            return;
        }
        // Auto-enqueue on search templates. WooCommerce product
        // search (`/?s=…&post_type=product`) also qualifies even when
        // the theme's archive template doesn't set is_search() early.
        $should = (function_exists('is_search') && is_search())
            || self::isWooCommerceProductSearch();
        $should = apply_filters('seekmodo_enqueue_beacon', $should);
        if (!$should) {
            return;
        }
        $handle  = 'seekmodo-beacon';
        $src     = SEEKMODO_WP_PLUGIN_URL . 'assets/typeahead/seekmodo-beacon.js';
        $version = SEEKMODO_WP_VERSION;
        wp_enqueue_script($handle, $src, [], $version, true);
        if (function_exists('wp_localize_script') && function_exists('rest_url')) {
            $seid = 0;
            if (class_exists(\Numinix\SeekmodoWordPress\Commerce\SearchSwap::class)) {
                $seid = (int) \Numinix\SeekmodoWordPress\Commerce\SearchSwap::lastSearchEventId();
            }
            wp_localize_script($handle, 'SEEKMODO_BEACON', [
                'endpoint'        => rest_url(Routes::NAMESPACE_NAME . '/click'),
                'nonce'           => function_exists('wp_create_nonce') ? wp_create_nonce('wp_rest') : '',
                'search_event_id' => $seid > 0 ? $seid : 0,
            ]);
        }
    }

    /**
     * @param mixed $request WP_REST_Request
     * @return array<string,mixed>
     */
    public static function handle($request): array
    {
        $keyword   = is_object($request) ? (string) $request->get_param('keyword') : '';
        $object_id = is_object($request) ? (int) $request->get_param('object_id') : 0;
        $position  = is_object($request) ? (int) $request->get_param('position') : 1;
        $surface   = is_object($request) ? (string) $request->get_param('surface') : 'serp';
        $searchEventId = is_object($request) ? (int) $request->get_param('search_event_id') : 0;
        if ($searchEventId <= 0) {
            $searchEventId = ShopperContext::lastSearchEventId();
        }

        if ($keyword === '' || $object_id <= 0) {
            return ['ok' => false, 'reason' => 'invalid_payload'];
        }

        // Unlinked SERP clicks dominate fleet linkage on high-bot
        // tenants (cached SERPs / scrapers POSTing keyword+pid with no
        // seid). Typeahead/suggest already stamp seid; refuse serp
        // clicks that lack one so LTR + fleet health stay clean.
        if ($searchEventId <= 0 && ($surface === '' || $surface === 'serp')) {
            return ['ok' => false, 'reason' => 'missing_search_event_id'];
        }

        $opts = ['surface' => $surface !== '' ? $surface : 'serp'];
        if ($searchEventId > 0) {
            $opts['search_event_id'] = $searchEventId;
        }

        self::queue($keyword, $object_id, $position, null, $opts);

        return ['ok' => true];
    }

    private static function isWooCommerceProductSearch(): bool
    {
        if (!class_exists('WooCommerce', false)) {
            return false;
        }
        if (!isset($_GET['s']) || !is_string($_GET['s']) || trim($_GET['s']) === '') {
            return false;
        }
        if (!isset($_GET['post_type'])) {
            return function_exists('is_search') && is_search();
        }
        $post_type = $_GET['post_type'];

        return $post_type === 'product'
            || (is_array($post_type) && in_array('product', $post_type, true));
    }

    /**
     * Queue a click event for the next flush tick.
     *
     * @param array<string,mixed> $opts
     */
    public static function queue(
        string $keyword,
        int $object_id,
        int $position,
        ?string $bot_verdict = null,
        array $opts = []
    ): void {
        $shopper = ShopperContext::forGateway();
        $surface = (string) ($opts['surface'] ?? 'serp');
        $seid = isset($opts['search_event_id']) ? (int) $opts['search_event_id'] : 0;
        if ($seid <= 0 && ($surface === '' || $surface === 'serp')) {
            return;
        }
        EventsQueue::push([
            'type'        => 'click',
            'keyword'     => $keyword,
            'object_id'   => $object_id,
            'position'    => $position,
            'bot_verdict' => $bot_verdict,
            'surface'     => $surface !== '' ? $surface : 'serp',
            'object_type' => $opts['object_type'] ?? 'post',
            'occurred_at' => time(),
            'search_event_id' => $seid,
            'session_id'  => $shopper['session_id'],
            'ua'          => $shopper['ua'],
            'ip'          => $shopper['ip'],
            'referer'     => $shopper['referer'],
        ]);
    }
}
