<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress\Content;

use Numinix\SeekmodoWordPress\Rest\Routes;

/**
 * Typeahead / autocomplete for the content vertical.
 *
 * Surfaces:
 *
 *   - REST `GET /wp-json/seekmodo/v1/typeahead?q=...&max=...&vertical=content`
 *   - Shortcode `[seekmodo_search vertical="content"]`
 *
 * The REST endpoint always returns a `{rows, fallback, latency_ms}`
 * shape regardless of whether the gateway answered or we fell back to
 * a native WP_Query LIKE search; theme JS never has to branch.
 *
 * Sprint 4 adds the `seekmodo-beacon.js` enqueue + Gutenberg block;
 * Sprint 1 ships the REST + shortcode so the trek.ca pilot has a
 * working typeahead surface.
 */
final class Typeahead
{
    public static function register(): void
    {
        if (!function_exists('add_action')) {
            return;
        }
        add_action('rest_api_init', [self::class, 'registerRoutes']);
        add_action('init', [self::class, 'registerShortcode']);
    }

    public static function registerRoutes(): void
    {
        if (!function_exists('register_rest_route')) {
            return;
        }
        register_rest_route(Routes::NAMESPACE_NAME, '/typeahead', [
            'methods'             => 'GET',
            'callback'            => [self::class, 'restHandler'],
            'permission_callback' => '__return_true',
            'args'                => [
                'q' => [
                    'required'          => true,
                    'sanitize_callback' => 'sanitize_text_field',
                ],
                'max' => [
                    'required'          => false,
                    'sanitize_callback' => 'absint',
                    'default'           => 8,
                ],
                'vertical' => [
                    'required'          => false,
                    'sanitize_callback' => 'sanitize_key',
                    'default'           => 'content',
                ],
            ],
        ]);
    }

    public static function registerShortcode(): void
    {
        if (!function_exists('add_shortcode')) {
            return;
        }
        add_shortcode('seekmodo_search', [self::class, 'shortcode']);
    }

    /**
     * @param mixed $request
     * @return array<string,mixed>
     */
    public static function restHandler($request): array
    {
        $q        = is_object($request) ? trim((string) $request->get_param('q')) : '';
        $max      = is_object($request) ? max(1, min(20, (int) $request->get_param('max'))) : 8;
        $vertical = is_object($request) ? (string) $request->get_param('vertical') : 'content';

        if ($q === '') {
            return ['rows' => [], 'fallback' => false, 'latency_ms' => 0];
        }

        $started = microtime(true);
        $rows    = numinix_seekmodo_run_typeahead($q, $max);
        $latency = (int) round((microtime(true) - $started) * 1000);

        $used_fallback = false;
        $shaped        = [];

        if (is_array($rows)) {
            foreach ($rows as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $shaped_row = $vertical === 'commerce'
                    ? $row
                    : self::shapeContentRow($row);
                $shaped[]   = apply_filters('seekmodo_typeahead_shape_row', $shaped_row, $vertical);
            }
            $shaped = array_values($shaped);
        }

        if ($vertical === 'content') {
            $had_gateway = $shaped !== [];
            $gateway_ids = self::rowIds($shaped);

            if ($had_gateway) {
                $shaped = self::mergeContentRows($shaped, $q, $max);
            } else {
                $shaped = self::nativeFallback($q, $max);
            }

            $shaped = apply_filters('seekmodo_typeahead_native_fallback', $shaped, $q, $max);
            $shaped = self::orderContentRows($shaped, $max);

            $used_fallback = !$had_gateway;
            if ($had_gateway) {
                foreach (self::rowIds($shaped) as $id) {
                    if (!in_array($id, $gateway_ids, true)) {
                        $used_fallback = true;
                        break;
                    }
                }
            }

            return [
                'rows'       => array_values($shaped),
                'fallback'   => $used_fallback,
                'latency_ms' => $latency,
            ];
        }

        if ($shaped !== []) {
            return [
                'rows'       => $shaped,
                'fallback'   => false,
                'latency_ms' => $latency,
            ];
        }

        // Native fallback when the gateway is unavailable or returned no rows.
        $native = self::nativeFallback($q, $max);
        $native = apply_filters('seekmodo_typeahead_native_fallback', $native, $q, $max);

        return ['rows' => array_values($native), 'fallback' => true, 'latency_ms' => $latency];
    }

    /**
     * Union gateway hits with native WP rows, then bucket-sort by the
     * operator's post-type priority (trek.ca: page before post).
     *
     * @param array<int,array<string,mixed>> $gatewayRows
     * @return array<int,array<string,mixed>>
     */
    private static function mergeContentRows(array $gatewayRows, string $q, int $max): array
    {
        $by_id = [];
        foreach ($gatewayRows as $row) {
            $id = (int) ($row['id'] ?? 0);
            if ($id > 0) {
                $by_id[$id] = $row;
            }
        }

        $pool = min(20, max($max * 2, $max));
        foreach (self::nativeFallback($q, $pool) as $row) {
            $id = (int) ($row['id'] ?? 0);
            if ($id > 0 && !isset($by_id[$id])) {
                $by_id[$id] = $row;
            }
        }

        return self::orderContentRows(array_values($by_id), $max);
    }

    /**
     * @param array<int,array<string,mixed>> $rows
     * @return array<int,array<string,mixed>>
     */
    private static function orderContentRows(array $rows, int $max): array
    {
        if ($rows === []) {
            return [];
        }
        $ids = PostTypePriority::applyOrder(self::rowIds($rows));
        $by_id = [];
        foreach ($rows as $row) {
            $id = (int) ($row['id'] ?? 0);
            if ($id > 0) {
                $by_id[$id] = $row;
            }
        }
        $out = [];
        foreach ($ids as $id) {
            if (isset($by_id[$id])) {
                $out[] = $by_id[$id];
            }
            if (count($out) >= $max) {
                break;
            }
        }

        return $out;
    }

    /**
     * @param array<int,array<string,mixed>> $rows
     * @return list<int>
     */
    private static function rowIds(array $rows): array
    {
        $ids = [];
        foreach ($rows as $row) {
            $id = (int) ($row['id'] ?? 0);
            if ($id > 0) {
                $ids[] = $id;
            }
        }

        return $ids;
    }

    /**
     * Map a gateway-shaped row (Typesense document fields) to the
     * stable storefront-facing typeahead shape that
     * `assets/typeahead/seekmodo-search.js` + downstream themes
     * consume.
     *
     * Gateway-side field names (per the content Typesense schema in
     * `services/mcp-gateway/ops/tenant-add.php`):
     *   id (string), title, body, author, tags, published_at, url,
     *   excerpt, thumbnail_url, post_type, is_sticky
     *
     * Storefront-facing keys are deliberately different so we don't
     * break trek.ca's `js/static.js` and the shortcode template
     * which already read `permalink` / `thumbnail_url` / `post_type`.
     * Legacy field names (`permalink` on input) are accepted as a
     * fallback so a tenant on an older gateway version keeps working
     * during the v1.0 -> v1.1 rollout.
     *
     * @param array<string,mixed> $row
     * @return array<string,mixed>
     */
    private static function shapeContentRow(array $row): array
    {
        $url = '';
        if (isset($row['url'])) {
            $url = (string) $row['url'];
        } elseif (isset($row['permalink'])) {
            $url = (string) $row['permalink'];
        }

        $post_id = isset($row['id']) ? (int) $row['id'] : 0;
        $thumb   = isset($row['thumbnail_url']) ? (string) $row['thumbnail_url'] : '';
        if ($thumb === '' && $post_id > 0) {
            $thumb = ThumbnailUrl::forPost($post_id);
        }

        return [
            'id'            => isset($row['id']) ? (string) $row['id'] : '',
            'title'         => isset($row['title']) ? (string) $row['title'] : '',
            'permalink'     => $url,
            'thumbnail_url' => $thumb,
            'post_type'     => isset($row['post_type']) ? (string) $row['post_type'] : 'post',
            'excerpt'       => isset($row['excerpt']) ? (string) $row['excerpt'] : '',
        ];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private static function nativeFallback(string $q, int $max): array
    {
        if (function_exists('numinix_seekmodo_enhanced_native_enabled')
            && numinix_seekmodo_enhanced_native_enabled()) {
            return \Numinix\SeekmodoWordPress\Search\EnhancedNativeRegistry::runContentTypeahead($q, $max);
        }
        if (!function_exists('get_posts')) {
            return [];
        }
        $posts = get_posts([
            's'                => $q,
            'post_type'        => PostTypeRegistry::indexable(),
            'post_status'      => 'publish',
            'posts_per_page'   => $max,
            'orderby'          => 'relevance',
            'no_found_rows'    => true,
            'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- batch/index query must bypass filters
        ]);
        if (!is_array($posts)) {
            return [];
        }
        $out = [];
        foreach ($posts as $p) {
            $post_id = (int) ($p->ID ?? 0);
            $excerpt = (string) ($p->post_excerpt ?? '');
            if ($excerpt === '' && function_exists('wp_strip_all_tags')) {
                $excerpt = mb_substr(
                    wp_strip_all_tags((string) ($p->post_content ?? '')),
                    0,
                    160
                );
            }
            $out[] = [
                'id'            => (string) $post_id,
                'title'         => (string) ($p->post_title ?? ''),
                'permalink'     => function_exists('get_permalink') ? (string) get_permalink($post_id) : '',
                'thumbnail_url' => ThumbnailUrl::forPost($post_id),
                'post_type'     => (string) ($p->post_type ?? 'post'),
                'excerpt'       => $excerpt,
            ];
        }

        return $out;
    }

    /**
     * @param array<string,string>|string $atts
     */
    public static function shortcode($atts = []): string
    {
        $atts = is_array($atts) ? $atts : [];
        $atts = array_merge([
            'vertical'    => 'content',
            'placeholder' => 'Search…',
        ], $atts);

        $endpoint = function_exists('rest_url') ? rest_url(Routes::NAMESPACE_NAME . '/typeahead') : '';

        ob_start();
        ?>
        <div class="seekmodo-typeahead-shortcode" data-seekmodo-vertical="<?php echo esc_attr($atts['vertical']); ?>">
            <input type="search" placeholder="<?php echo esc_attr($atts['placeholder']); ?>"
                   class="seekmodo-typeahead-input"
                   data-seekmodo-endpoint="<?php echo esc_attr($endpoint); ?>"
                   autocomplete="off" />
            <div class="seekmodo-typeahead-dropdown" hidden></div>
        </div>
        <?php

        return (string) ob_get_clean();
    }
}
