<?php

declare(strict_types=1);

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Procedural facades for the Seekmodo connector. Themes and other
 * plugins call these — never the underlying classes — so the public
 * surface stays small and `function_exists()`-guardable.
 *
 * Versioning rules (mirrors Zen Cart connector docs/INTEGRATION.md):
 *   - Adding a new helper is fine.
 *   - Adding optional args with safe defaults is fine.
 *   - Renaming is forbidden; ship a new helper + alias if needed.
 *   - Changing return shape requires a major-version bump.
 *
 * Every helper short-circuits to `null` (or a no-op) when the
 * connector is disabled (MODE=off, breaker open, credentials missing,
 * storefront-host locked out). Callers can therefore wire all four
 * swap-points without any orchestration of their own — the connector
 * handles the "is this even running?" question internally.
 *
 * @package Numinix\SeekmodoWordPress
 */

use Numinix\SeekmodoWordPress\Config\DomainLock;
use Numinix\SeekmodoWordPress\Config\Options;
use Numinix\SeekmodoWordPress\Events\ClickBeacon;
use Numinix\SeekmodoWordPress\Gateway\Client;
use Numinix\SeekmodoWordPress\Mode\AutoPromoter;
use Numinix\SeekmodoWordPress\Mode\ModeFsm;

if (!function_exists('numinix_seekmodo_enabled')) {
    /**
     * True when the Seekmodo gateway path is active (paired, not off, domain lock).
     * Alias for backward compatibility — same as numinix_seekmodo_gateway_enabled().
     */
    function numinix_seekmodo_enabled(): bool
    {
        return numinix_seekmodo_gateway_enabled();
    }
}

if (!function_exists('numinix_seekmodo_gateway_enabled')) {
    /**
     * Gateway swap path: paired + not MODE=off + domain lock match.
     */
    function numinix_seekmodo_gateway_enabled(): bool
    {
        if (!Options::isPaired()) {
            return false;
        }
        if (ModeFsm::current() === ModeFsm::MODE_OFF) {
            return false;
        }
        if (!DomainLock::matches()) {
            return false;
        }

        return true;
    }
}

if (!function_exists('numinix_seekmodo_enhanced_native_enabled')) {
    /**
     * Enhanced Native layer runs without pairing when true (default).
     * Filter `seekmodo_enhanced_native_enabled` for incident response.
     */
    function numinix_seekmodo_enhanced_native_enabled(): bool
    {
        if (function_exists('apply_filters')) {
            return (bool) apply_filters('seekmodo_enhanced_native_enabled', true);
        }

        return true;
    }
}

if (!function_exists('numinix_seekmodo_mode')) {
    /**
     * Current *operator-set* operational mode (the "headline" mode).
     * One of: off, shadow, active, enforce. This is what the
     * admin.seekmodo.com console wrote into the connector — it does
     * NOT reflect the AutoPromoter's substate.
     *
     * For "is Seekmodo currently serving visitor traffic?" use
     * `numinix_seekmodo_effective_mode()` instead.
     */
    function numinix_seekmodo_mode(): string
    {
        return ModeFsm::current();
    }
}

if (!function_exists('numinix_seekmodo_effective_mode')) {
    /**
     * Effective operational mode (headline mode projected through
     * the AutoPromoter substate). One of: off, shadow, active,
     * enforce.
     *
     * Mapping:
     *   - off                                 -> off
     *   - shadow (operator pin)               -> shadow
     *   - enforce (operator pin)              -> enforce
     *   - active + auto_promote=0             -> enforce
     *   - active + substate=shadow            -> shadow
     *   - active + substate=enforce           -> enforce
     *
     * Theme / plugin code that wants to render "Seekmodo is serving
     * this page" badges, or to gate non-search behavior on the
     * actual user-visible mode, should call this rather than
     * `numinix_seekmodo_mode()`.
     */
    function numinix_seekmodo_effective_mode(): string
    {
        return AutoPromoter::effectiveMode();
    }
}

if (!function_exists('numinix_seekmodo_show_branding')) {
    /**
     * Whether the storefront should render a "Powered by Seekmodo" badge
     * (or any other Seekmodo branding) in its search UI.
     *
     * Source of truth lives on the gateway: each tenant has a
     * `show_seekmodo_branding` flag (TINYINT, default 0) on
     * `numinix_mcp_tenant_config`. The flag is pulled into the connector
     * by {@see RemoteConfig::applyOverrides()} on every 5-min snapshot
     * and persisted to {@see Options::OPT_SHOW_BRANDING} for fast local
     * reads. Off by default; operators turn it on per-tenant for partner
     * sites and explicit-deal customers.
     *
     * Themes / plugins can override the final answer via the
     * `seekmodo_show_branding` filter — useful for partner storefronts
     * that want the badge lit immediately while the operator-side toggle
     * is still being rolled out, and for the inverse case where a tenant
     * never wants the badge regardless of what the gateway snapshot says.
     *
     * Returns `false` even when the option is set if the connector is
     * not paired (no tenant ID) — an unpaired connector is not in a
     * position to take credit for a tenant's search experience.
     */
    function numinix_seekmodo_show_branding(): bool
    {
        $show = Options::isPaired() ? Options::showBranding() : false;

        if (function_exists('apply_filters')) {
            $show = (bool) apply_filters('seekmodo_show_branding', $show);
        }

        return (bool) $show;
    }
}

if (!function_exists('numinix_seekmodo_brand_url')) {
    /**
     * Canonical URL for the "Powered by Seekmodo" badge. Themes that
     * render the badge should link to this URL with target="_blank"
     * rel="noopener noreferrer" so the click leaves the storefront
     * cleanly without leaking the referrer.
     *
     * Filterable via `seekmodo_brand_url` for partner storefronts that
     * have a co-branded landing page on seekmodo.com they want clicks to
     * land on instead.
     */
    function numinix_seekmodo_brand_url(): string
    {
        $url = 'https://seekmodo.com/';

        if (function_exists('apply_filters')) {
            $url = (string) apply_filters('seekmodo_brand_url', $url);
        }

        return $url;
    }
}

if (!function_exists('numinix_seekmodo_run_search')) {
    /**
     * Run a search against the Seekmodo gateway. Returns a
     * `['ids' => int[], 'total' => int, 'meta' => array]` shape on
     * success, or `null` to signal "fall back to native". Callers
     * MUST handle the null case.
     *
     * @param array<string,mixed> $params Search params (q, page, page_size, filters, etc).
     * @return array<string,mixed>|null
     */
    function numinix_seekmodo_run_search(array $params): ?array
    {
        if (!numinix_seekmodo_enabled()) {
            return null;
        }

        return Client::search($params);
    }
}

if (!function_exists('numinix_seekmodo_run_typeahead')) {
    /**
     * Run a typeahead query. Returns rows or `null` on fallback.
     *
     * @param string $q Query string.
     * @param int    $max Max rows.
     * @return array<int,array<string,mixed>>|null
     */
    function numinix_seekmodo_run_typeahead(string $q, int $max = 8): ?array
    {
        if (!numinix_seekmodo_enabled()) {
            return null;
        }

        return Client::typeahead($q, $max);
    }
}

if (!function_exists('numinix_seekmodo_can_index')) {
    /**
     * Stricter, write-side companion to {@see numinix_seekmodo_enabled()}.
     *
     * Returns false on every state where READS are blocked (paired,
     * MODE!=off, host-lock match) AND additionally fails closed when
     * the host looks non-prod (`dev.*`, `staging.*`, etc.) and the
     * lock was self-seeded — the exact regression PM-303 hit when
     * dev product saves repopulated POCO's Typesense index with
     * `dev.pocomarine.com` URLs after the prod cutover.
     *
     * Operators that legitimately need a non-prod install to index
     * (e.g. paired to a separate `*-dev` tenant) opt back in via:
     *
     *   add_filter('seekmodo_allow_nonprod_indexing', '__return_true');
     *
     * Read-only `seekmodo_disable_storefront_host_lock` also unlocks
     * writes (CDN blue/green pattern).
     *
     * Used by every gateway-mutating helper (`numinix_seekmodo_run_bulk_upsert`,
     * `numinix_seekmodo_mirror_click`).
     */
    function numinix_seekmodo_can_index(): bool
    {
        if (!Options::isPaired()) {
            return false;
        }
        if (ModeFsm::current() === ModeFsm::MODE_OFF) {
            return false;
        }

        return DomainLock::canIndex();
    }
}

if (!function_exists('numinix_seekmodo_run_bulk_upsert')) {
    /**
     * Push a batch of documents to the gateway's index endpoint.
     * Called by the indexer cron (and `wp seekmodo index`).
     *
     * @param array<int,array<string,mixed>> $docs       Batch of docs (cap at 500).
     * @param string                         $collection Vertical: 'content' or 'commerce'.
     * @return array{accepted:int,rejected:int}|null
     */
    function numinix_seekmodo_run_bulk_upsert(array $docs, string $collection): ?array
    {
        // v0.5.3 — uses the stricter write-side gate
        // (`numinix_seekmodo_can_index()` instead of the read-side
        // `numinix_seekmodo_enabled()`) so a self-referential
        // dev-host lock can't repopulate the prod Typesense
        // collection. See PM-303 for the regression that drove this.
        if (!numinix_seekmodo_can_index()) {
            return null;
        }

        return Client::bulkUpsert($docs, $collection);
    }
}

if (!function_exists('numinix_seekmodo_mirror_click')) {
    /**
     * Mirror a click event to the gateway. Non-blocking: the event is
     * queued to the local events table and flushed by cron, so even an
     * unreachable gateway never adds latency to the visitor's request.
     *
     * @param string               $keyword    Original search keyword.
     * @param int                  $object_id  Post / product / row id clicked.
     * @param int                  $position   1-based position in the result set.
     * @param string|null          $bot_verdict 'human'|'bot'|null.
     * @param array<string,mixed>  $opts        Optional. ['surface' => 'serp'|'typeahead'|...].
     */
    function numinix_seekmodo_mirror_click(
        string $keyword,
        int $object_id,
        int $position,
        ?string $bot_verdict = null,
        array $opts = []
    ): void {
        // Click-mirroring is a gateway-side write (events queue +
        // analytics row), so it gates on the stricter
        // `numinix_seekmodo_can_index()` like the indexer batch path.
        // Read-only typeahead / search-swap helpers continue to gate
        // on the looser `numinix_seekmodo_enabled()` so a non-prod
        // install can still smoke-test search behavior.
        if (!numinix_seekmodo_can_index()) {
            return;
        }

        ClickBeacon::queue($keyword, $object_id, $position, $bot_verdict, $opts);
    }
}

if (!function_exists('numinix_seekmodo_mirror_typeahead_click')) {
    /**
     * Convenience alias for `numinix_seekmodo_mirror_click()` with
     * `surface=typeahead`. Provided so theme code can be self-documenting.
     */
    function numinix_seekmodo_mirror_typeahead_click(
        string $keyword,
        int $object_id,
        int $position,
        ?string $bot_verdict = null
    ): void {
        numinix_seekmodo_mirror_click($keyword, $object_id, $position, $bot_verdict, ['surface' => 'typeahead']);
    }
}

if (!function_exists('numinix_seekmodo_local_search')) {
    /**
     * Run a local inverted-index search (Enhanced Native).
     *
     * @param array<string,mixed> $params
     * @return array{ids: list<int>, total: int, source: string}|null
     */
    function numinix_seekmodo_local_search(array $params): ?array
    {
        return \Numinix\SeekmodoWordPress\LocalIndex\LocalSearch::search($params);
    }
}

if (!function_exists('seekmodo_do_query')) {
    /**
     * Apply local index results to a WP_Query (grouped / secondary searches).
     *
     * @param mixed $query WP_Query
     */
    function seekmodo_do_query($query): void
    {
        \Numinix\SeekmodoWordPress\LocalIndex\RelevanssiCompat::runQuery($query);
    }
}

if (!function_exists('relevanssi_do_query')) {
    /**
     * Relevanssi-compat alias when Relevanssi plugin is not active.
     *
     * @param mixed $query WP_Query
     */
    function relevanssi_do_query($query): void
    {
        if (\Numinix\SeekmodoWordPress\LocalIndex\VerticalConfig::relevanssiCompatEnabled()) {
            seekmodo_do_query($query);
        }
    }
}

if (!function_exists('seekmodo_get_search_excerpt')) {
    function seekmodo_get_search_excerpt(int $post_id, string $query, int $length = 240): string
    {
        return \Numinix\SeekmodoWordPress\LocalIndex\ExcerptBuilder::forPost($post_id, $query, $length);
    }
}

if (!function_exists('seekmodo_highlight_title')) {
    function seekmodo_highlight_title(string $title, string $query): string
    {
        return \Numinix\SeekmodoWordPress\LocalIndex\ExcerptBuilder::highlightTitle($title, $query);
    }
}
