<?php
/**
 * Global admin search — orders, customers, products.
 */
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Whether the current user can use global search (at least one entity type).
 */
function zerti_global_search_user_can_search(): bool
{
    if (!function_exists('zerti_user_can_access_admin') || !zerti_user_can_access_admin()) {
        return false;
    }

    return (function_exists('zerti_user_can_view_shop_orders_grid') && zerti_user_can_view_shop_orders_grid())
        || (function_exists('zerti_user_can_view_customers_grid') && zerti_user_can_view_customers_grid())
        || (function_exists('zerti_user_can_view_products_grid') && zerti_user_can_view_products_grid());
}

/**
 * Client config for global search.
 *
 * @return array<string, mixed>
 */
function zerti_global_search_client_config(): array
{
    return [
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'nonce' => wp_create_nonce('zerti_global_search'),
        'action' => 'zerti_global_search',
        'minLength' => 2,
        'debounceMs' => 280,
        'limitPerGroup' => 6,
        'can' => [
            'orders' => function_exists('zerti_user_can_view_shop_orders_grid') && zerti_user_can_view_shop_orders_grid(),
            'customers' => function_exists('zerti_user_can_view_customers_grid') && zerti_user_can_view_customers_grid(),
            'products' => function_exists('zerti_user_can_view_products_grid') && zerti_user_can_view_products_grid(),
        ],
        'i18n' => [
            'placeholder' => __('Search orders, customers, products…', 'zerti'),
            'openSearch' => __('Open search', 'zerti'),
            'closeSearch' => __('Close search', 'zerti'),
            'clear' => __('Clear search', 'zerti'),
            'loading' => __('Searching…', 'zerti'),
            'tooShort' => __('Type at least 2 characters.', 'zerti'),
            'empty' => __('No results found.', 'zerti'),
            'orders' => __('Orders', 'zerti'),
            'customers' => __('Customers', 'zerti'),
            'products' => __('Products', 'zerti'),
        ],
    ];
}

/**
 * Minimum normalized length for global search (toolbar uses 2; grids use 3).
 */
function zerti_global_search_min_term_length(): int
{
    return 2;
}

/**
 * Normalize a global search term (Persian/Arabic digits, ی/ي, ZWNJ, case).
 */
function zerti_global_search_normalized_term(string $term): string
{
    return zerti_shop_orders_grid_search_normalize(trim($term));
}

/**
 * Whether a raw search term is long enough after normalization.
 */
function zerti_global_search_term_is_valid(string $term): bool
{
    $needle = zerti_global_search_normalized_term($term);

    return $needle !== ''
        && zerti_shop_orders_grid_search_length($needle) >= zerti_global_search_min_term_length();
}

/**
 * Substring match on a grid row searchText field with unified normalization.
 *
 * @param array<string, mixed> $row
 */
function zerti_global_search_row_matches(array $row, string $term): bool
{
    if (!zerti_global_search_term_is_valid($term)) {
        return false;
    }

    $needle = zerti_global_search_normalized_term($term);
    $haystack = zerti_global_search_normalized_term((string) ($row['searchText'] ?? ''));

    return function_exists('mb_stripos')
        ? mb_stripos($haystack, $needle, 0, 'UTF-8') !== false
        : stripos($haystack, $needle) !== false;
}

/**
 * Order row match — searchText plus billing/customer fields used by the orders grid.
 *
 * @param array<string, mixed> $row
 */
function zerti_global_search_order_row_matches(array $row, string $term): bool
{
    if (!zerti_global_search_term_is_valid($term)) {
        return false;
    }

    $needle = zerti_global_search_normalized_term($term);
    $haystack = zerti_global_search_normalized_term(trim(implode(' ', array_filter([
        (string) ($row['searchText'] ?? ''),
        (string) ($row['customerSearch'] ?? ''),
        (string) ($row['customer'] ?? ''),
    ]))));

    return function_exists('mb_stripos')
        ? mb_stripos($haystack, $needle, 0, 'UTF-8') !== false
        : stripos($haystack, $needle) !== false;
}

/**
 * Try to append one order to global search results.
 *
 * @param list<array{id:int,label:string,meta:string,sub:string}> $results
 * @param array<int, true> $seen
 */
function zerti_global_search_try_add_order(WC_Order $order, array &$results, array &$seen, int $limit): bool
{
    if (!is_a($order, 'WC_Order') || !zerti_shop_order_grid_include_in_list($order)) {
        return false;
    }

    $order_id = (int) $order->get_id();
    if ($order_id <= 0 || isset($seen[$order_id]) || !zerti_user_can_view_shop_order($order_id)) {
        return false;
    }

    $results[] = zerti_global_search_format_order($order);
    $seen[$order_id] = true;

    return count($results) >= $limit;
}

/**
 * Max customer/product scan batches (batch size 200) per global search request.
 */
function zerti_global_search_max_entity_scan_batches(): int
{
    return max(1, (int) apply_filters('zerti_global_search_max_entity_scan_batches', 5));
}

/**
 * Order IDs whose billing fields or ID match a SQL LIKE probe (HPOS or legacy).
 *
 * @return list<int>
 */
function zerti_global_search_order_ids_from_db(string $term, int $limit = 80): array
{
    global $wpdb;

    $limit = max(1, min(200, $limit));
    $term = trim($term);
    if ($term === '' || !function_exists('zerti_shop_orders_grid_statuses_for_sql')) {
        return [];
    }

    $patterns = array_values(array_unique(array_filter([
        $term,
        zerti_to_latin_digits($term),
    ])));
    if ($patterns === []) {
        return [];
    }

    $query_args = function_exists('zerti_shop_orders_grid_normalize_query_args')
        ? zerti_shop_orders_grid_normalize_query_args(['orderby' => 'date', 'order' => 'DESC'])
        : ['orderby' => 'date', 'order' => 'DESC'];
    $statuses_sql = zerti_shop_orders_grid_statuses_for_sql($query_args);
    if ($statuses_sql === []) {
        return [];
    }

    if (!function_exists('zerti_wc_home_uses_hpos')) {
        require_once dirname(__DIR__) . '/zerti-wc-home-stats.php';
    }

    $match_sql_parts = [];
    $match_params = [];
    foreach ($patterns as $pattern) {
        $like = '%' . $wpdb->esc_like($pattern) . '%';
        if (zerti_wc_home_uses_hpos()) {
            $match_sql_parts[] = '(ba.first_name LIKE %s OR ba.last_name LIKE %s OR ba.email LIKE %s OR CAST(o.id AS CHAR) LIKE %s)';
        } else {
            $match_sql_parts[] = '(billing.billing_first LIKE %s OR billing.billing_last LIKE %s OR billing.billing_email LIKE %s OR billing.order_id_text LIKE %s)';
        }
        array_push($match_params, $like, $like, $like, $like);
    }
    $match_sql = implode(' OR ', $match_sql_parts);

    $status_placeholders = implode(', ', array_fill(0, count($statuses_sql), '%s'));
    $params = array_merge($match_params, $statuses_sql, [$limit]);

    if (zerti_wc_home_uses_hpos()) {
        $orders_table = "{$wpdb->prefix}wc_orders";
        $addresses_table = "{$wpdb->prefix}wc_order_addresses";
        $sql = "SELECT DISTINCT o.id AS order_id
            FROM {$orders_table} o
            LEFT JOIN {$addresses_table} ba ON ba.order_id = o.id AND ba.address_type = 'billing'
            WHERE o.type = 'shop_order'
                AND o.status IN ({$status_placeholders})
                AND ({$match_sql})
            ORDER BY o.date_created_gmt DESC
            LIMIT %d";
    } else {
        $sql = "SELECT DISTINCT o.ID AS order_id
            FROM {$wpdb->posts} o
            INNER JOIN (
                SELECT p.ID AS oid,
                    CAST(p.ID AS CHAR) AS order_id_text,
                    MAX(CASE WHEN pm.meta_key = '_billing_first_name' THEN pm.meta_value END) AS billing_first,
                    MAX(CASE WHEN pm.meta_key = '_billing_last_name' THEN pm.meta_value END) AS billing_last,
                    MAX(CASE WHEN pm.meta_key = '_billing_email' THEN pm.meta_value END) AS billing_email
                FROM {$wpdb->posts} p
                LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
                    AND pm.meta_key IN ('_billing_first_name', '_billing_last_name', '_billing_email')
                WHERE p.post_type = 'shop_order'
                GROUP BY p.ID
            ) billing ON billing.oid = o.ID
            WHERE o.post_type = 'shop_order'
                AND o.post_status IN ({$status_placeholders})
                AND ({$match_sql})
            ORDER BY o.post_date_gmt DESC
            LIMIT %d";
    }

    // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- dynamic SQL built above.
    $prepared = $wpdb->prepare($sql, $params);
    if ($prepared === false) {
        return [];
    }
    // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $prepared from $wpdb->prepare() above.
    $col = $wpdb->get_col($prepared);
    if (!is_array($col) || ($wpdb->last_error !== '' && defined('WP_DEBUG') && WP_DEBUG)) {
        return [];
    }

    $ids = [];
    foreach ($col as $id) {
        $id = absint($id);
        if ($id > 0) {
            $ids[] = $id;
        }
    }

    return $ids;
}

/**
 * Output the global search bar in the admin shell.
 */
function zerti_render_global_search_bar(): void
{
    if (!zerti_global_search_user_can_search()) {
        return;
    }

    $cfg = zerti_global_search_client_config();
    ?>
    <div
        class="zerti-global-search"
        data-zerti-global-search
        data-zerti-global-search-min="<?php echo esc_attr((string) ($cfg['minLength'] ?? 2)); ?>"
    >
        <button
            type="button"
            class="zerti-global-search__toggle"
            data-zerti-global-search-toggle
            aria-expanded="false"
            aria-controls="zertiGlobalSearchField"
            aria-label="<?php echo esc_attr($cfg['i18n']['openSearch']); ?>"
        >
            <i class="fa-solid fa-magnifying-glass zerti-theme-switch-ico" aria-hidden="true"></i>
        </button>
        <div class="zerti-global-search__body" data-zerti-global-search-body hidden>
            <label class="zerti-global-search__field" id="zertiGlobalSearchField">
                <span class="visually-hidden"><?php echo esc_html($cfg['i18n']['placeholder']); ?></span>
                <input
                    type="search"
                    class="zerti-global-search__input"
                    data-zerti-global-search-input
                    autocomplete="off"
                    spellcheck="false"
                    enterkeyhint="search"
                    role="combobox"
                    aria-autocomplete="list"
                    aria-expanded="false"
                    aria-controls="zertiGlobalSearchResults"
                    placeholder="<?php echo esc_attr($cfg['i18n']['placeholder']); ?>"
                />
                <button
                    type="button"
                    class="zerti-global-search__clear"
                    data-zerti-global-search-clear
                    hidden
                    aria-label="<?php echo esc_attr($cfg['i18n']['clear']); ?>"
                >
                    <i class="fa-solid fa-xmark" aria-hidden="true"></i>
                </button>
            </label>
            <div
                id="zertiGlobalSearchResults"
                class="zerti-global-search__panel"
                data-zerti-global-search-panel
                hidden
                role="listbox"
            ></div>
        </div>
    </div>
    <?php
}

/**
 * Format one order for global search results.
 *
 * @return array{id:int,label:string,meta:string,sub:string}
 */
function zerti_global_search_format_order(WC_Order $order): array
{
    $row = zerti_shop_order_grid_row($order);
    $id = (int) $order->get_id();
    $label = sprintf(
        '#%s — %s',
        zerti_to_persian_digits((string) $id),
        (string) ($row['customer'] ?? '')
    );
    $sub_parts = array_filter([
        (string) ($row['date'] ?? ''),
        trim(wp_strip_all_tags((string) ($row['totalHtml'] ?? ''))),
    ]);

    return [
        'id' => $id,
        'label' => $label,
        'meta' => (string) ($row['statusText'] ?? ''),
        'sub' => implode(' · ', $sub_parts),
    ];
}

/**
 * Search WooCommerce orders for global search.
 *
 * @return list<array{id:int,label:string,meta:string,sub:string}>
 */
function zerti_global_search_orders(string $term, int $limit = 6): array
{
    if (!zerti_user_can_view_shop_orders_grid() || !function_exists('wc_get_orders')) {
        return [];
    }

    $limit = max(1, min(10, $limit));
    $results = [];
    $seen = [];
    $term_latin = trim(zerti_to_latin_digits($term));

    if ($term_latin !== '' && preg_match('/^\d+$/', $term_latin)) {
        $order_id = absint($term_latin);
        if ($order_id > 0 && zerti_user_can_view_shop_order($order_id)) {
            $order = wc_get_order($order_id);
            if ($order && is_a($order, 'WC_Order')) {
                zerti_global_search_try_add_order($order, $results, $seen, $limit);
            }
        }
    }

    if (count($results) >= $limit || !zerti_global_search_term_is_valid($term)) {
        return array_slice($results, 0, $limit);
    }

    foreach (zerti_global_search_order_ids_from_db($term, max($limit * 4, 40)) as $order_id) {
        if (count($results) >= $limit) {
            break;
        }
        if (isset($seen[$order_id])) {
            continue;
        }
        $order = wc_get_order($order_id);
        if (!$order || !is_a($order, 'WC_Order')) {
            continue;
        }
        $row = zerti_shop_order_grid_row($order);
        if (!zerti_global_search_order_row_matches($row, $term)) {
            continue;
        }
        zerti_global_search_try_add_order($order, $results, $seen, $limit);
    }

    return array_slice($results, 0, $limit);
}

/**
 * Build one customer global-search hit from a grid row.
 *
 * @param array<string, mixed> $row
 * @return array{id:int,label:string,meta:string,sub:string}|null
 */
function zerti_global_search_format_customer_row(array $row): ?array
{
    $customer_id = isset($row['idRaw']) ? absint($row['idRaw']) : 0;
    if ($customer_id <= 0) {
        return null;
    }
    if (function_exists('zerti_user_can_view_customer_detail') && !zerti_user_can_view_customer_detail($customer_id)) {
        return null;
    }

    return [
        'id' => $customer_id,
        'label' => (string) ($row['name'] ?? ''),
        'meta' => (string) ($row['email'] ?? ''),
        'sub' => (string) ($row['phone'] ?? ''),
    ];
}

/**
 * Try to add a customer from a WP_User using grid-row matching.
 *
 * @param list<array{id:int,label:string,meta:string,sub:string}> $results
 * @param array<int, true> $seen
 */
function zerti_global_search_try_add_customer(WP_User $user, string $term, array &$results, array &$seen, int $limit): bool
{
    if (!$user instanceof WP_User) {
        return false;
    }
    $customer_id = (int) $user->ID;
    if ($customer_id <= 0 || isset($seen[$customer_id])) {
        return false;
    }

    $row = zerti_customer_grid_row($user);
    if (!zerti_global_search_row_matches($row, $term)) {
        return false;
    }

    $formatted = zerti_global_search_format_customer_row($row);
    if ($formatted === null) {
        return false;
    }

    $results[] = $formatted;
    $seen[$customer_id] = true;

    return count($results) >= $limit;
}

/**
 * Fast customer lookup via core user search + billing meta (before full scan).
 *
 * @param array<int, true> $seen
 * @return list<array{id:int,label:string,meta:string,sub:string}>
 */
function zerti_global_search_customers_fast(string $term, int $limit, array &$seen): array
{
    $results = [];
    $fetch = max($limit * 4, 24);
    $variants = array_values(array_unique(array_filter([
        trim($term),
        trim(zerti_to_latin_digits($term)),
    ])));

    $user_query = new WP_User_Query([
        'role' => 'customer',
        'number' => $fetch,
        'orderby' => 'ID',
        'order' => 'DESC',
        'search' => '*' . trim($term) . '*',
        'search_columns' => ['user_login', 'user_email', 'display_name'],
        'fields' => 'all',
    ]);
    foreach ($user_query->get_results() as $user) {
        if (zerti_global_search_try_add_customer($user, $term, $results, $seen, $limit)) {
            return $results;
        }
    }

    if (count($results) >= $limit) {
        return $results;
    }

    $meta_or = ['relation' => 'OR'];
    foreach ($variants as $variant) {
        if ($variant === '') {
            continue;
        }
        foreach (['billing_first_name', 'billing_last_name', 'billing_phone', 'shipping_phone'] as $meta_key) {
            $meta_or[] = [
                'key' => $meta_key,
                'value' => $variant,
                'compare' => 'LIKE',
            ];
        }
    }
    if (count($meta_or) <= 1) {
        return $results;
    }

    $meta_query = new WP_User_Query([
        'role' => 'customer',
        'number' => $fetch,
        'orderby' => 'ID',
        'order' => 'DESC',
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Admin global search; bounded customer lookup.
        'meta_query' => $meta_or,
        'fields' => 'all',
    ]);
    foreach ($meta_query->get_results() as $user) {
        if (zerti_global_search_try_add_customer($user, $term, $results, $seen, $limit)) {
            return $results;
        }
    }

    return $results;
}

/**
 * Search customers for global search.
 *
 * @return list<array{id:int,label:string,meta:string,sub:string}>
 */
function zerti_global_search_customers(string $term, int $limit = 6): array
{
    if (!zerti_user_can_view_customers_grid() || !function_exists('zerti_customer_grid_row')) {
        return [];
    }

    if (!zerti_global_search_term_is_valid($term)) {
        return [];
    }

    $limit = max(1, min(10, $limit));
    $results = [];
    $seen = [];

    $results = zerti_global_search_customers_fast($term, $limit, $seen);
    if (count($results) >= $limit) {
        return array_slice($results, 0, $limit);
    }

    $scan_page = 1;
    $scan_limit = 200;
    $max_batches = zerti_global_search_max_entity_scan_batches();

    while ($scan_page <= $max_batches && count($results) < $limit) {
        $users = get_users([
            'role' => 'customer',
            'number' => $scan_limit,
            'offset' => ($scan_page - 1) * $scan_limit,
            'orderby' => 'ID',
            'order' => 'ASC',
            'fields' => 'all',
        ]);
        if (!is_array($users) || $users === []) {
            break;
        }

        foreach ($users as $user) {
            if (zerti_global_search_try_add_customer($user, $term, $results, $seen, $limit)) {
                break 2;
            }
        }

        if (count($users) < $scan_limit) {
            break;
        }
        $scan_page++;
    }

    return array_slice($results, 0, $limit);
}

/**
 * Search products for global search.
 *
 * @return list<array{id:int,label:string,meta:string,sub:string}>
 */
function zerti_global_search_products(string $term, int $limit = 6): array
{
    if (!function_exists('zerti_user_can_view_products_grid') || !zerti_user_can_view_products_grid()) {
        return [];
    }
    if (!function_exists('wc_get_products') || !function_exists('zerti_product_grid_row') || !function_exists('zerti_products_grid_wc_query_args')) {
        return [];
    }

    if (!zerti_global_search_term_is_valid($term)) {
        return [];
    }

    $limit = max(1, min(10, $limit));
    $results = [];
    $seen = [];

    if (function_exists('zerti_product_search_for_picker') && function_exists('wc_get_product')) {
        foreach (zerti_product_search_for_picker($term, 0, max($limit * 3, 18)) as $item) {
            if (!is_array($item)) {
                continue;
            }
            $product_id = isset($item['id']) ? absint($item['id']) : 0;
            if ($product_id <= 0 || isset($seen[$product_id])) {
                continue;
            }
            $product = wc_get_product($product_id);
            if (!$product || !is_a($product, 'WC_Product') || $product->is_type('variation')) {
                continue;
            }
            $row = zerti_product_grid_row($product);
            if (!zerti_global_search_row_matches($row, $term)) {
                continue;
            }
            $seen[$product_id] = true;
            $sku = trim((string) $product->get_sku());
            $results[] = [
                'id' => $product_id,
                'label' => (string) ($row['name'] ?? ''),
                'meta' => $sku !== '' ? sprintf(/* translators: %s: product SKU */ __('SKU: %s', 'zerti'), $sku) : '',
                'sub' => (string) ($row['type'] ?? ''),
            ];
            if (count($results) >= $limit) {
                return $results;
            }
        }
    }

    $scan_page = 1;
    $scan_limit = 200;
    $max_batches = zerti_global_search_max_entity_scan_batches();
    $parsed = [
        'limit' => $scan_limit,
        'orderby' => 'ID',
        'order' => 'ASC',
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Scan uses ID order; meta_key unused here.
        'meta_key' => '',
    ];

    while ($scan_page <= $max_batches && count($results) < $limit) {
        $scan_args = zerti_products_grid_wc_query_args($parsed, $scan_page);
        $scan_args['paginate'] = true;
        $batch = wc_get_products($scan_args);
        $products = is_object($batch) && isset($batch->products) && is_array($batch->products)
            ? $batch->products
            : [];
        if ($products === []) {
            break;
        }

        foreach ($products as $product) {
            if (!is_a($product, 'WC_Product') || $product->is_type('variation')) {
                continue;
            }
            $product_id = (int) $product->get_id();
            if (isset($seen[$product_id])) {
                continue;
            }
            $row = zerti_product_grid_row($product);
            if (!zerti_global_search_row_matches($row, $term)) {
                continue;
            }
            $seen[$product_id] = true;
            $sku = trim((string) $product->get_sku());
            $results[] = [
                'id' => $product_id,
                'label' => (string) ($row['name'] ?? ''),
                'meta' => $sku !== '' ? sprintf(/* translators: %s: product SKU */ __('SKU: %s', 'zerti'), $sku) : '',
                'sub' => (string) ($row['type'] ?? ''),
            ];
            if (count($results) >= $limit) {
                break 2;
            }
        }

        if (count($products) < $scan_limit) {
            break;
        }
        $scan_page++;
    }

    return $results;
}

/**
 * AJAX: grouped global search.
 */
function zerti_ajax_global_search(): void
{
    check_ajax_referer('zerti_global_search', 'nonce');

    if (!zerti_global_search_user_can_search()) {
        wp_send_json_error(['message' => 'forbidden'], 403);
    }

    if (function_exists('wp_raise_memory_limit')) {
        wp_raise_memory_limit('admin');
    }
    if (function_exists('set_time_limit')) {
        @set_time_limit(90); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- allow longer admin AJAX search on shared hosting.
    }

    $term = isset($_POST['term']) ? sanitize_text_field(wp_unslash((string) $_POST['term'])) : '';
    $term = trim($term);

    if ($term === '' || !function_exists('mb_strlen') || mb_strlen($term, 'UTF-8') < zerti_global_search_min_term_length()) {
        wp_send_json_success([
            'orders' => [],
            'customers' => [],
            'products' => [],
            'tooShort' => true,
        ]);
    }

    $limit = isset($_POST['limit']) ? absint(wp_unslash((string) $_POST['limit'])) : 6;
    $limit = max(1, min(10, $limit));

    wp_send_json_success([
        'products' => zerti_global_search_products($term, $limit),
        'customers' => zerti_global_search_customers($term, $limit),
        'orders' => zerti_global_search_orders($term, $limit),
        'tooShort' => false,
    ]);
}
