/**
 * SearchProvider Component
 *
 * Provides Seekora client and context to child components.
 * Supports A/B testing via abTestId/abVariant props.
 */
import React, { createContext, useContext, useMemo, useEffect, } from "react";
import { log, SearchStateManager, } from "@seekora-ai/ui-sdk-core";
import { defaultTheme } from "../themes/default";
import { createTheme } from "../themes/createTheme";
const SearchContext = createContext(null);
export const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, autoTrackSearch = true, stateManager: stateManagerConfig, children, abTestId, abVariant, experiments: _experiments, trackContext, userId, initialResultsPromise, initialQuery, }) => {
    const theme = useMemo(() => {
        return themeConfig ? createTheme(themeConfig) : defaultTheme;
    }, [themeConfig]);
    // Create state manager instance (memoized to avoid recreating on every render)
    const stateManager = useMemo(() => {
        return new SearchStateManager({
            client,
            autoSearch: true,
            debounceMs: 300,
            itemsPerPage: 10,
            defaultSearchOptions: { widget_mode: true },
            ...stateManagerConfig,
            abTestId,
            abVariant,
            trackContext,
            // Thread server-rendered boot results into the state manager so the
            // first search() call skips a network round-trip (SCRUM-268).
            ...(initialResultsPromise != null
                ? { initialResultsPromise, initialQuery: initialQuery ?? "" }
                : {}),
        });
        // trackContext is propagated via setTrackContext below — leaving it
        // out of deps avoids recreating the manager (and losing in-flight
        // pagination/refinement state) when consumers swap analytics ctx.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [client, stateManagerConfig, abTestId, abVariant]);
    // Update A/B test fields on state manager and SDK client when props change
    useEffect(() => {
        if (abTestId !== undefined || abVariant !== undefined) {
            stateManager.setAbTest(abTestId, abVariant);
            client.setAbTest(abTestId, abVariant);
        }
    }, [stateManager, client, abTestId, abVariant]);
    // Keep state-manager's trackContext in sync with props. setTrackContext
    // just swaps the emission target — no state reset. When the prop first
    // becomes non-null this unlocks V4 emissions on subsequent searches.
    // userId is merged into trackContext when both are present.
    useEffect(() => {
        const contextWithUserId = trackContext && userId ? { ...trackContext, userId } : trackContext;
        stateManager.setTrackContext(contextWithUserId ?? null);
    }, [stateManager, trackContext, userId]);
    // Destroy the state manager when the provider unmounts or when the memoized
    // instance is replaced (new client, stateManagerConfig, etc.). Prevents
    // dangling debounce timers and coalesce resolvers from leaking in React 18
    // strict mode (double-mount/unmount) and during SPA route navigations. SCRUM-268.
    useEffect(() => {
        return () => {
            try {
                stateManager.destroy();
            }
            catch {
                /* swallow */
            }
        };
    }, [stateManager]);
    const value = useMemo(() => ({
        client,
        theme,
        enableAnalytics,
        autoTrackSearch,
        stateManager,
        trackContext: trackContext ?? null,
    }), [
        client,
        theme,
        enableAnalytics,
        autoTrackSearch,
        stateManager,
        trackContext,
    ]);
    return (React.createElement(SearchContext.Provider, { value: value }, children));
};
// Lazy-built no-op fallback context — returned when components are mounted
// OUTSIDE any <SearchProvider> (e.g. shopify-app theme extension stand-alone
// UMD mounts via the seekora-react-sdk UMD). All client/stateManager methods
// are no-ops so components don't crash. Components that need real state
// should use the prop-based control flow (facets/onFacetChange, results-prop,
// pagination/onPageChange) instead of relying on context.
let __noopContext = null;
const __buildNoopContext = () => {
    if (__noopContext)
        return __noopContext;
    // Synthetic minimal SearchState — matches packages/core/SearchState shape
    // that useSearchState.getState() / subscribe() consume.
    // SearchState.refinements is an ARRAY of `{ field, value, ... }` records
    // (Facets calls `refinements.some(...)` / `refinements.find(...)`), NOT
    // a `Record<field, value[]>` map. Same for facets — must be array form.
    const emptyState = {
        query: "",
        refinements: [],
        currentPage: 1,
        itemsPerPage: 24,
        sortBy: "",
        results: null,
        loading: false,
        error: null,
        facets: [],
    };
    const noop = (..._args) => undefined;
    const asyncNoop = (..._args) => Promise.resolve({});
    // Explicit known methods short-circuit the Proxy below. Anything else
    // (getFiltersSchema, searchFacetValues, etc. — methods the SDK adds
    // over time) falls through to the Proxy and returns a Promise-resolving
    // noop so callers like `.then(...)` chain cleanly.
    const stateMethods = {
        getState: () => emptyState,
        subscribe: (_listener) => () => { },
        setQuery: noop,
        addRefinement: noop,
        removeRefinement: noop,
        clearRefinements: noop,
        setPage: noop,
        setSortBy: noop,
        search: asyncNoop,
        clear: noop,
        setTrackContext: noop,
        setAbTest: noop,
        setUserId: noop,
        getFiltersSchema: asyncNoop,
        searchFacetValues: asyncNoop,
    };
    const stubStateManager = new Proxy(stateMethods, {
        get(target, prop) {
            if (prop in target)
                return target[prop];
            // Unknown method — return a noop that resolves to a Promise (most
            // SDK methods are async). Callers awaiting / .then'ing get safe shapes.
            return asyncNoop;
        },
    });
    const stubClient = new Proxy({}, {
        get(_t, prop) {
            if (prop === "then")
                return undefined;
            // Default no-op for any method call on the client.
            return noop;
        },
    });
    __noopContext = {
        client: stubClient,
        theme: defaultTheme,
        enableAnalytics: false,
        autoTrackSearch: false,
        stateManager: stubStateManager,
        trackContext: null,
    };
    return __noopContext;
};
export const useSearchContext = () => {
    const context = useContext(SearchContext);
    if (!context) {
        // Don't throw — fall back to a no-op synthetic context so components
        // mounted standalone (e.g. shopify-app theme extension chrome mounts
        // via the seekora-react-sdk UMD) keep rendering with prop-based state
        // instead of crashing the whole storefront. Log once for visibility.
        if (typeof window !== "undefined" && !window.__seekoraCtxWarned) {
            window.__seekoraCtxWarned = true;
            log.warn("SearchProvider: no provider — using no-op fallback context (prop-based components only)");
        }
        return __buildNoopContext();
    }
    return context;
};
/**
 * Convenience hook for components that emit V4 analytics events.
 *
 * Returns the `TrackContext` supplied to `SearchProvider`, or `null` if
 * the provider was rendered without one. Components like `useImpressionTracker`
 * are safe to call this — they treat `null` as a no-op, so a missing
 * trackContext doesn't crash the tree at SSR/first-paint time.
 *
 * Returns null (rather than throwing) when used outside SearchProvider so
 * a stray `useTrackContext()` in a deeply-nested component doesn't blow up
 * storybook / SSR previews. Components that NEED an active trackContext
 * should check the return and bail out (matches buildImpressionEffect's
 * null-guard contract).
 */
export const useTrackContext = () => {
    const context = useContext(SearchContext);
    return context?.trackContext ?? null;
};
