<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress\Commerce;

use Numinix\SeekmodoWordPress\Mode\AutoPromoter;
use Numinix\SeekmodoWordPress\Mode\ModeFsm;
use Numinix\SeekmodoWordPress\Mode\ShadowDiffLogger;
use Numinix\SeekmodoWordPress\Search\EnhancedNativeRegistry;

/**
 * WooCommerce search swap. Mirrors the Content/SearchSwap pattern but
 * hooks `woocommerce_product_query` (Woo's main product archive query)
 * plus `posts_search` (the SQL WHERE builder) so we can short-circuit
 * Woo's native LIKE query.
 */
final class SearchSwap
{
    private const SWAP_FLAG = 'seekmodo_commerce_swap_active';

    /** Set for the current request when enforce-mode swap reorders SERP. */
    private static bool $serpClickTracking = false;

    /** Guard against double-swap when both hooks fire on the same query. */
    private static bool $swapHandled = false;

    /**
     * Keyword preserved for SERP chrome after we clear `s` on the query.
     * Themes call get_search_query() for titles/breadcrumbs; without this
     * they render `Search results: ""` even though $_GET['s'] is set.
     */
    private static ?string $displayKeyword = null;

    /** Gateway meta.search_event_id from the latest commerce swap this request. */
    private static int $lastSearchEventId = 0;

    public static function serpClickTrackingActive(): bool
    {
        return self::$serpClickTracking;
    }

    public static function lastSearchEventId(): int
    {
        return self::$lastSearchEventId;
    }

    public static function register(): void
    {
        if (!function_exists('add_action')) {
            return;
        }
        // Run late so WooCommerce's product_query() finishes setting
        // orderby/catalog args before we stamp post__in + post__in order.
        // Priority 9 ran too early: WC overwrote our ranked IDs and, with
        // `s` cleared, the storefront fell back to the shop category grid.
        add_action('woocommerce_product_query', [self::class, 'onProductQuery'], 999, 1);
        add_action('pre_get_posts',              [self::class, 'preGetPosts'], 999);
        add_filter('posts_search',              [self::class, 'onPostsSearch'], 10, 2);
        add_filter('get_search_query',           [self::class, 'filterSearchQuery'], 10, 1);
        add_filter('woocommerce_maybe_show_product_subcategories', [self::class, 'suppressSubcategories'], 10, 1);
    }

    /**
     * @param mixed $show
     * @return mixed
     */
    public static function suppressSubcategories($show)
    {
        if (self::$swapHandled) {
            return false;
        }

        return $show;
    }

    /**
     * @param mixed $query
     * @return mixed
     */
    public static function filterSearchQuery($query)
    {
        if (self::$displayKeyword !== null && trim((string) $query) === '') {
            return self::$displayKeyword;
        }

        return $query;
    }

    /**
     * @param mixed $query
     */
    public static function preGetPosts($query): void
    {
        if (self::$swapHandled || !is_object($query) || !method_exists($query, 'is_main_query')) {
            return;
        }
        if (!$query->is_main_query() || is_admin()) {
            return;
        }
        if (!method_exists($query, 'is_search') || !$query->is_search()) {
            return;
        }
        $post_type = $query->get('post_type');
        $is_product = $post_type === 'product'
            || (is_array($post_type) && in_array('product', $post_type, true));
        if (!$is_product) {
            return;
        }

        self::onProductQuery($query);
    }

    /**
     * @param mixed $q WC_Query / WP_Query.
     */
    public static function onProductQuery($q): void
    {
        if (self::$swapHandled) {
            return;
        }

        if (!self::isSwappable($q)) {
            return;
        }

        $keyword = is_object($q) && method_exists($q, 'get') ? trim((string) $q->get('s')) : '';
        if ($keyword === '') {
            return;
        }

        $page      = self::pagedFromQuery($q);
        $page_size = self::pageSizeFromQuery($q);
        $params = [
            'q'         => $keyword,
            'page'      => $page,
            'page_size' => $page_size,
            'lang'      => function_exists('get_locale') ? get_locale() : 'en_US',
            'vertical'  => 'commerce',
            'filters'   => self::extractFilters(),
        ];

        $started_at = microtime(true);
        $resp       = null;
        if (function_exists('numinix_seekmodo_gateway_enabled') && numinix_seekmodo_gateway_enabled()) {
            $resp = numinix_seekmodo_run_search($params);
        }
        $latency_ms = (int) round((microtime(true) - $started_at) * 1000);

        $mode = AutoPromoter::effectiveMode();

        if ($mode === ModeFsm::MODE_SHADOW) {
            if (is_array($resp) && isset($resp['ids']) && is_array($resp['ids'])) {
                ShadowDiffLogger::log($keyword, [], array_values(array_map('intval', $resp['ids'])), $latency_ms);
            }
            self::applyEnhancedNative($q, $keyword, $page, $page_size);

            return;
        }

        if (!is_array($resp) || !isset($resp['ids']) || !is_array($resp['ids'])) {
            self::applyEnhancedNative($q, $keyword, $page, $page_size);

            return;
        }

        $ids = array_values(array_filter(array_map('intval', $resp['ids'])));
        if (empty($ids)) {
            self::applyEnhancedNative($q, $keyword, $page, $page_size);

            return;
        }

        EnhancedNativeRegistry::recordGatewayIds($keyword, $ids);
        $total = isset($resp['total']) ? (int) $resp['total'] : count($ids);
        self::rememberSearchEventId($resp);
        self::applyRankedIds($q, $ids, $keyword, $total);
    }

    /**
     * @param array<string,mixed> $resp
     */
    private static function rememberSearchEventId(array $resp): void
    {
        $meta = is_array($resp['meta'] ?? null) ? $resp['meta'] : [];
        $seid = isset($meta['search_event_id']) ? (int) $meta['search_event_id'] : 0;
        if ($seid > 0) {
            self::$lastSearchEventId = $seid;
            if (class_exists(\Numinix\SeekmodoWordPress\Gateway\ShopperContext::class)) {
                \Numinix\SeekmodoWordPress\Gateway\ShopperContext::rememberSearchEventId($seid);
            }
        }
    }

    /**
     * @param mixed $search
     * @param mixed $query
     * @return mixed
     */
    public static function onPostsSearch($search, $query)
    {
        if (!is_object($query) || !method_exists($query, 'get')) {
            return $search;
        }
        if (!$query->get(self::SWAP_FLAG)) {
            return $search;
        }

        return '';
    }

    /**
     * @param mixed $q
     */
    private static function applyEnhancedNative($q, string $keyword, int $page, int $page_size): void
    {
        if (!function_exists('numinix_seekmodo_enhanced_native_enabled')
            || !numinix_seekmodo_enhanced_native_enabled()) {
            return;
        }
        $enhanced = EnhancedNativeRegistry::runCommerceSearch($keyword, $page, $page_size);
        if ($enhanced === null || $enhanced['ids'] === []) {
            if (is_object($q) && method_exists($q, 'set')) {
                $q->set('post__in', [0]);
                $q->set(self::SWAP_FLAG, true);
                self::$serpClickTracking = true;
                self::$swapHandled       = true;
            }

            return;
        }
        self::applyRankedIds($q, $enhanced['ids'], $keyword, $enhanced['total']);
    }

    /**
     * @param mixed $q
     * @param list<int> $ids
     */
    private static function applyRankedIds($q, array $ids, string $keyword, int $total): void
    {
        if (!is_object($q) || !method_exists($q, 'set')) {
            return;
        }
        self::$displayKeyword = $keyword;
        $q->set('post__in', $ids);
        $q->set('orderby', 'post__in');
        $q->set('post_type', 'product');
        $q->set(self::SWAP_FLAG, true);
        $page_size = self::pageSizeFromQuery($q);
        $q->found_posts   = max($total, count($ids));
        $q->max_num_pages = $page_size > 0
            ? (int) ceil($q->found_posts / $page_size)
            : 1;
        self::$serpClickTracking = true;
        self::$swapHandled       = true;
    }

    /**
     * @param mixed $q
     */
    private static function isSwappable($q): bool
    {
        if (!is_object($q) || !method_exists($q, 'get')) {
            return false;
        }
        if (is_admin()) {
            return false;
        }
        if (!function_exists('numinix_seekmodo_enhanced_native_enabled')
            || !numinix_seekmodo_enhanced_native_enabled()) {
            if (!function_exists('numinix_seekmodo_gateway_enabled') || !numinix_seekmodo_gateway_enabled()) {
                return false;
            }
        }
        $post_type = (string) $q->get('post_type');
        if ($post_type !== '' && $post_type !== 'product') {
            return false;
        }
        $search = (string) $q->get('s');

        return $search !== '';
    }

    /**
     * @param mixed $q
     */
    private static function pagedFromQuery($q): int
    {
        if (!is_object($q) || !method_exists($q, 'get')) {
            return 1;
        }
        $paged = (int) $q->get('paged');

        return $paged > 0 ? $paged : 1;
    }

    /**
     * @param mixed $q
     */
    private static function pageSizeFromQuery($q): int
    {
        if (!is_object($q) || !method_exists($q, 'get')) {
            return 12;
        }
        $size = (int) $q->get('posts_per_page');
        if ($size <= 0) {
            $size = function_exists('wc_get_loop_prop')
                ? (int) wc_get_loop_prop('per_page', 12)
                : 12;
        }

        return $size > 0 ? min($size, 100) : 12;
    }

    /**
     * @return array<string,mixed>
     */
    private static function extractFilters(): array
    {
        $filters = [];

        if (!empty($_GET['min_price'])) {
            $filters['price_min'] = (float) $_GET['min_price'];
        }
        if (!empty($_GET['max_price'])) {
            $filters['price_max'] = (float) $_GET['max_price'];
        }

        foreach ($_GET as $k => $v) {
            if (!is_string($k)) {
                continue;
            }
            $attr = null;
            if (str_starts_with($k, 'filter_')) {
                $attr = substr($k, 7);
            } elseif (str_starts_with($k, 'pa_')) {
                $attr = substr($k, 3);
            }
            if ($attr === null || $attr === '') {
                continue;
            }
            $values = is_array($v) ? $v : preg_split('/\s*,\s*/', (string) $v);
            $values = array_values(array_filter(array_map(static fn ($x) => sanitize_text_field((string) $x), (array) $values)));
            if (!empty($values)) {
                $filters['attributes'][$attr] = $values;
            }
        }

        if (!empty($_GET['rating_filter'])) {
            $filters['rating_min'] = (int) $_GET['rating_filter'];
        }

        return $filters;
    }
}
