/**
 * SearchResults Component
 *
 * Displays search results with customizable rendering
 * Includes keyboard navigation support (arrow keys, Enter to select)
 */
import React, { useState, useRef, useEffect, useCallback } from "react";
import { useSearchContext, useTrackContext } from "./SearchProvider";
import { useSearchState } from "../hooks/useSearchState";
import { log } from "@seekora-ai/ui-sdk-core";
import { clsx } from "clsx";
import { trackProductListViewed } from "@seekora-ai/search-sdk";
import { BufferProvider } from "./buffers/BufferProvider";
import { useObjectClickedBuffer } from "../hooks/useObjectClickedBuffer";
import { useSearchImpressionBuffer } from "../hooks/useSearchImpressionBuffer";
import { useObjectViewedBuffer } from "../hooks/useObjectViewedBuffer";
import { ensureSeekoraStyles } from "../styles/inject";
import { resolveFieldMapping, } from "../utils/field-mapping-presets";
import { extractTextFromHtml } from "../utils/html-text";
import { ThemeHarvestGrid } from "./ThemeHarvestGrid";
// Helper function to get nested value from object using dot notation
const getNestedValue = (obj, path) => {
    if (!path)
        return undefined;
    return path.split(".").reduce((current, key) => current?.[key], obj);
};
// Helper function to format price
const formatPrice = (value, currency = "₹") => {
    if (value === undefined || value === null)
        return undefined;
    if (typeof value === "number")
        return `${currency}${value}`;
    if (typeof value === "string") {
        const num = parseFloat(value);
        if (!isNaN(num))
            return `${currency}${num}`;
        return value;
    }
    return String(value);
};
/** Normalise a url field to an absolute path. Bare handles get /products/ prefix. */
const toProductUrl = (url) => {
    if (!url)
        return undefined;
    if (url.startsWith("http://") || url.startsWith("https://"))
        return url;
    if (url.startsWith("/"))
        return url;
    return `/products/${url}`;
};
/** Renders a description paragraph either as plain text (HTML-stripped via
 *  extractTextFromHtml) or as raw HTML when asHtml is true (caller's responsibility
 *  to ensure content is safe). When lineClamp is set, applies -webkit-line-clamp
 *  so the description is truncated to that many lines. SCRUM-268. */
const DescriptionParagraph = ({ className, html, asHtml, lineClamp }) => {
    const clampStyle = lineClamp != null
        ? {
            display: "-webkit-box",
            WebkitBoxOrient: "vertical",
            WebkitLineClamp: lineClamp,
            overflow: "hidden",
        }
        : undefined;
    if (asHtml) {
        // eslint-disable-next-line react/no-danger
        return (React.createElement("p", { className: className, style: clampStyle, dangerouslySetInnerHTML: { __html: html } }));
    }
    return (React.createElement("p", { className: className, style: clampStyle }, extractTextFromHtml(html)));
};
/**
 * Auto-detect the theme's main section ID from the DOM.
 * Reads `#shopify-section-<id>` wrapper on the current page.
 */
function detectThemeSectionId() {
    const el = document.querySelector('[id*="__main"].shopify-section') ||
        document.querySelector('[id*="__search"].shopify-section');
    if (!el)
        return null;
    return el.id.replace("shopify-section-", "");
}
/**
 * Fetches theme-rendered cards via Section Rendering API from the correct
 * page URL context (/search or /collections/<handle>). Extracts individual
 * card elements and caches by a fingerprint of the fetch URL.
 */
function fetchThemeCards(sectionId) {
    const pageUrl = window.location.pathname + window.location.search;
    const sep = pageUrl.includes("?") ? "&" : "?";
    const url = `${pageUrl}${sep}section_id=${encodeURIComponent(sectionId)}`;
    return fetch(url, { headers: { Accept: "text/html" } })
        .then((r) => (r.ok ? r.text() : Promise.reject(new Error(`${r.status}`))))
        .then((text) => {
        const doc = new DOMParser().parseFromString(text, "text/html");
        // Extract <style> for subsetted CSS
        const styleEl = doc.querySelector("style[data-section-stylesheet]");
        if (styleEl && !document.querySelector(`#seekora-theme-card-css`)) {
            const s = document.createElement("style");
            s.id = "seekora-theme-card-css";
            s.textContent = styleEl.textContent || "";
            document.head.appendChild(s);
        }
        const cards = doc.querySelectorAll("li[data-product-id], li.product-grid__item, product-card, [data-product-card], .product-card");
        if (cards.length > 0)
            return [...cards].map((c) => c.outerHTML);
        const grid = doc.querySelector(".product-grid, [data-product-grid], #product-grid, results-list") || doc.body;
        const items = grid.querySelectorAll("li, article, .grid__item");
        return [...items].map((c) => c.outerHTML);
    })
        .catch(() => []);
}
/**
 * Renders theme cards fetched from Shopify's Section Rendering API.
 * Fetches from the current page URL (/search or /collections/*) so the
 * Liquid context (search.results / collection.products) is available.
 * Re-fetches when query/page changes. Analytics tracking wired.
 */
const ThemeSectionGrid = ({ query, sectionId, resultItems, onResultClick, className }) => {
    const containerRef = useRef(null);
    const [cardHtmls, setCardHtmls] = useState([]);
    const clickBuffer = useObjectClickedBuffer();
    const impressionBuffer = useSearchImpressionBuffer();
    const viewedBuffer = useObjectViewedBuffer();
    const { results } = useSearchState();
    const searchId = results?.context?.searchId ||
        results?.context?.search_id ||
        results?.data?.search_id ||
        "";
    const resolvedSectionId = React.useMemo(() => (sectionId === "auto" ? detectThemeSectionId() : sectionId), [sectionId]);
    // Fetch theme cards from the current page URL context
    useEffect(() => {
        if (!resolvedSectionId)
            return;
        let cancelled = false;
        fetchThemeCards(resolvedSectionId).then((htmls) => {
            if (!cancelled)
                setCardHtmls(htmls);
        });
        return () => {
            cancelled = true;
        };
        // Re-fetch when query changes (new search) but NOT on every resultItems change
        // (facets use Seekora results, theme cards are the visual shell)
    }, [query, resolvedSectionId]);
    // Impression tracking
    useEffect(() => {
        if (!containerRef.current || cardHtmls.length === 0)
            return;
        const cards = containerRef.current.querySelectorAll("[data-seekora-theme-card]");
        const observer = new IntersectionObserver((entries) => {
            for (const entry of entries) {
                if (!entry.isIntersecting)
                    continue;
                const idx = Number(entry.target.dataset.seekoraIndex);
                const item = resultItems[idx];
                if (item) {
                    const objectData = {
                        object_type: "product",
                        ...(item.title ? { name: item.title } : {}),
                    };
                    impressionBuffer.record(String(item.id), idx + 1, searchId, objectData, "results_page");
                    // Object Viewed — universal item impression, fired alongside
                    // Search Impression on every card surface (search + collection).
                    // Separate buffer, separate event; dedupe across the two on the
                    // search page is a downstream concern. SCRUM-267.
                    viewedBuffer.record(String(item.id), idx + 1, objectData, "results_page");
                }
                observer.unobserve(entry.target);
            }
        }, { threshold: 0.5 });
        cards.forEach((c) => observer.observe(c));
        return () => observer.disconnect();
    }, [cardHtmls, resultItems, impressionBuffer, viewedBuffer, searchId]);
    const handleCardClick = useCallback((e) => {
        const card = e.target.closest("[data-seekora-theme-card]");
        if (!card)
            return;
        const idx = Number(card.dataset.seekoraIndex);
        const item = resultItems[idx];
        if (item) {
            clickBuffer.record(String(item.id), idx + 1, searchId, {
                object_type: "product",
                ...(item.title ? { name: item.title } : {}),
            }, "results_page");
            if (onResultClick)
                onResultClick(item, idx);
        }
    }, [resultItems, clickBuffer, searchId, onResultClick]);
    if (cardHtmls.length === 0) {
        return (React.createElement("div", { className: className }, resultItems.map((_, i) => (React.createElement("div", { key: i, className: "seekora-results__card", style: { minHeight: 200, background: "#f8f9fa", borderRadius: 6 } })))));
    }
    return (React.createElement("div", { ref: containerRef, className: className, onClick: handleCardClick }, cardHtmls.map((html, i) => (React.createElement("div", { key: i, "data-seekora-theme-card": "", "data-seekora-index": i, dangerouslySetInnerHTML: { __html: html } })))));
};
/**
 * Internal `SearchResults` body. The exported `SearchResults` wraps this
 * with `<BufferProvider>` so click recording inside the result-card
 * handlers can land in the V4 batched buffer.
 *
 * Why split? `useObjectClickedBuffer()` has to be called from inside
 * the provider's React subtree — wrapping in the same component would
 * be a missing-provider error on first render. The Inner sits BELOW
 * the provider so the buffer hook resolves correctly.
 */
const SearchResultsInner = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = "list", fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, minHeight = "400px", minWidth = "100%", descriptionAsHtml = false, showHeader = false, showDescription = true, descriptionLines, cardMode = "seekora", cardSectionId, themeHarvestTemplate, themeHarvestSwapMap, themeHarvestMoneyFormat, }) => {
    const { theme, enableAnalytics } = useSearchContext();
    const clickBuffer = useObjectClickedBuffer();
    const impressionBuffer = useSearchImpressionBuffer();
    const viewedBuffer = useObjectViewedBuffer();
    const trackCtx = useTrackContext();
    // Surface discriminator read from the layout's `data-seekora-surface`
    // ancestor (set by LayoutRenderer): "search" on /search, "collection" on
    // /collections/*. Drives the non-search events (Product List Viewed +
    // Object Clicked Outside Search). Read in an effect (DOM-only) and cached
    // so click handlers can branch synchronously. SCRUM-267.
    const surfaceRef = useRef("search");
    const { results: stateResults, loading: stateLoading, error: stateError, currentPage, itemsPerPage: stateItemsPerPage, query: stateQuery, } = useSearchState();
    const searchResultsTheme = customTheme || {};
    // Keyboard navigation state
    const [activeIndex, setActiveIndex] = useState(-1);
    const containerRef = useRef(null);
    // Inject canonical stylesheet once on mount
    useEffect(() => {
        ensureSeekoraStyles();
    }, []);
    // Use props if provided, otherwise use state from state manager
    const results = resultsProp !== undefined ? resultsProp : stateResults;
    const loading = loadingProp !== undefined ? loadingProp : stateLoading;
    const error = errorProp !== undefined ? errorProp : stateError;
    // Reset active index when results change
    useEffect(() => {
        setActiveIndex(-1);
    }, [results]);
    // V4 Search Impression tracking — IntersectionObserver per card.
    //
    // Records ONE Search Impression per card the first time it scrolls into
    // viewport (threshold 0.5). The batched impressionBuffer dedupes within
    // a (search_id, surface) bucket so duplicate firings collapse before
    // hitting the wire; we still disconnect each card's observer after first
    // intersection to avoid keeping a long-lived observer per result.
    //
    // Mirrors useCardAnalytics' impression path (used by the ProductCard
    // primitive). SearchResults inline cards previously had no impression
    // wiring at all — explains why Search Impression rows never landed in
    // CH from the storefront hijack even when clicks worked. SCRUM-267
    // storefront-impression regression.
    useEffect(() => {
        const root = containerRef.current;
        if (!root || !results)
            return undefined;
        const items = Array.isArray(results.results)
            ? results.results
            : Array.isArray(results.items)
                ? results.items
                : [];
        if (items.length === 0)
            return undefined;
        const searchContext = (results.context ?? {});
        const searchId = searchContext.searchId ??
            searchContext.search_id;
        if (!searchId)
            return undefined;
        const page = currentPage || 1;
        const perPage = stateItemsPerPage || 24;
        const cards = root.querySelectorAll(".seekora-results__card");
        if (cards.length === 0)
            return undefined;
        const observer = new IntersectionObserver((entries) => {
            for (const entry of entries) {
                if (!entry.isIntersecting)
                    continue;
                const el = entry.target;
                const idxAttr = el.getAttribute("data-result-index");
                if (idxAttr === null)
                    continue;
                const idx = Number(idxAttr);
                const result = items[idx];
                if (!result || !result.id)
                    continue;
                const absolutePosition = (page - 1) * perPage + idx + 1;
                const objectData = {
                    object_type: "product",
                    ...(result.title ? { name: result.title } : {}),
                    ...(result.price ? { price: result.price } : {}),
                };
                try {
                    impressionBuffer.record(String(result.id), absolutePosition, searchId, objectData, "results_page");
                    // Object Viewed — universal item impression, fired alongside
                    // Search Impression on every card surface (search + collection).
                    // Separate buffer/event; search-page dedupe is downstream. SCRUM-267.
                    viewedBuffer.record(String(result.id), absolutePosition, objectData, "results_page");
                }
                catch {
                    // Buffer is null-safe; swallow defensively in case the
                    // analytics-next instance crashed mid-session.
                }
                observer.unobserve(el);
            }
        }, { threshold: 0.5 });
        cards.forEach((c) => observer.observe(c));
        return () => observer.disconnect();
        // impressionBuffer is stable from BufferProvider; results/page change
        // re-wires the observer.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [results, currentPage, stateItemsPerPage]);
    // Cache the surface ("search" | "collection" | ...) from the layout's
    // data-seekora-surface ancestor so click handlers can branch synchronously.
    useEffect(() => {
        const host = containerRef.current?.closest("[data-seekora-surface]");
        const s = host?.getAttribute("data-seekora-surface");
        if (s)
            surfaceRef.current = s;
    }, [results]);
    // V4 "Product List Viewed" (event 17) — fires ONCE per non-search results
    // grid load (collection / category page). Search pages emit Search
    // Submitted/Impression instead; this is the non-search twin. Batched
    // arrays mirror Search Impression. SCRUM-267.
    const plvFiredRef = useRef("");
    useEffect(() => {
        if (surfaceRef.current === "search")
            return;
        if (!results)
            return;
        const items = Array.isArray(results.results)
            ? results.results
            : Array.isArray(results.items)
                ? results.items
                : [];
        if (items.length === 0)
            return;
        const page = currentPage || 1;
        const perPage = stateItemsPerPage || 24;
        // Dedupe one emit per (surface, page, item-set) so re-renders of the same
        // result set don't double-fire.
        const ids = items.map((it) => String(it.id)).filter(Boolean);
        const fingerprint = `${surfaceRef.current}:${page}:${ids.join(",")}`;
        if (plvFiredRef.current === fingerprint)
            return;
        plvFiredRef.current = fingerprint;
        if (!trackCtx)
            return;
        try {
            trackProductListViewed(trackCtx, {
                object_ids: ids,
                object_positions: ids.map((_, i) => (page - 1) * perPage + i + 1),
                object_data: items.map((it) => ({
                    object_type: "product",
                    ...(it.title ? { name: it.title } : {}),
                    ...(it.price ? { price: it.price } : {}),
                })),
                list_id: (typeof window !== "undefined" &&
                    window.location.pathname.replace(/^\/collections\//, "")) ||
                    undefined,
                surface: "shopify_collection",
                _source: "ui_sdk",
            });
        }
        catch {
            // ctx is null-safe upstream; swallow defensively.
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [results, currentPage, stateItemsPerPage]);
    // Scroll active item into view
    useEffect(() => {
        if (activeIndex >= 0 && containerRef.current && enableKeyboardNavigation) {
            const activeEl = containerRef.current.querySelector(`[data-result-index="${activeIndex}"]`);
            if (activeEl) {
                activeEl.scrollIntoView({ block: "nearest", behavior: "smooth" });
            }
        }
    }, [activeIndex, enableKeyboardNavigation]);
    // Global keyboard listener for seamless navigation from search box to results
    useEffect(() => {
        if (!enableKeyboardNavigation)
            return;
        const handleGlobalKeyDown = (e) => {
            // Only handle if we have results and the container exists
            if (!containerRef.current)
                return;
            const resultElements = containerRef.current.querySelectorAll("[data-result-index]");
            const maxIndex = resultElements.length - 1;
            if (maxIndex < 0)
                return;
            // Scope to Seekora inputs only — do not intercept arrow keys typed into
            // unrelated inputs elsewhere on the page. SCRUM-268 fix #17.
            const activeElement = document.activeElement;
            const isInputFocused = activeElement?.tagName === "INPUT" ||
                activeElement?.tagName === "TEXTAREA";
            if (isInputFocused) {
                const isSeekoraInput = activeElement?.matches("[data-seekora-search-input]") ||
                    activeElement?.closest('[data-seekora-surface="search"]') !== null;
                if (!isSeekoraInput)
                    return;
            }
            if (e.key === "ArrowDown") {
                // If in search box or no result selected, select first result
                if (isInputFocused || activeIndex === -1) {
                    e.preventDefault();
                    setActiveIndex(0);
                    containerRef.current?.focus();
                }
            }
            else if (e.key === "ArrowUp") {
                // If first result is selected, go back to search box
                if (activeIndex === 0) {
                    e.preventDefault();
                    setActiveIndex(-1);
                    // Find and focus the search input scoped to the same surface
                    const surface = containerRef.current?.closest('[data-seekora-surface="search"]');
                    const searchInput = (surface || document).querySelector("[data-seekora-search-input]");
                    searchInput?.focus();
                }
            }
        };
        document.addEventListener("keydown", handleGlobalKeyDown);
        return () => document.removeEventListener("keydown", handleGlobalKeyDown);
    }, [enableKeyboardNavigation, activeIndex]);
    // Keyboard navigation handler
    const handleKeyDown = useCallback((e) => {
        if (!enableKeyboardNavigation)
            return;
        // We need to get the result count dynamically
        const resultElements = containerRef.current?.querySelectorAll("[data-result-index]");
        const maxIndex = resultElements ? resultElements.length - 1 : -1;
        switch (e.key) {
            case "ArrowDown":
                e.preventDefault();
                setActiveIndex((prev) => Math.min(prev + 1, maxIndex));
                break;
            case "ArrowUp":
                e.preventDefault();
                setActiveIndex((prev) => Math.max(prev - 1, -1));
                break;
            case "Enter":
                e.preventDefault();
                if (activeIndex >= 0) {
                    // Trigger click on active item
                    const activeEl = containerRef.current?.querySelector(`[data-result-index="${activeIndex}"]`);
                    if (activeEl) {
                        activeEl.click();
                    }
                }
                break;
            case "Escape":
                setActiveIndex(-1);
                containerRef.current?.blur();
                break;
            case "Home":
                e.preventDefault();
                setActiveIndex(0);
                break;
            case "End":
                e.preventDefault();
                setActiveIndex(maxIndex);
                break;
        }
    }, [enableKeyboardNavigation, activeIndex]);
    const defaultRenderResultList = (result, index, isActive = false) => {
        const handleClick = () => {
            // Record V4 click into the batched buffer (search-attributed).
            // The buffer flushes ONE `Object Clicked` per (search_id, surface)
            // bucket via its idle/unload triggers — no per-click HTTP call.
            //
            // V4 buffer record is NOT gated on `enableAnalytics`. That flag was
            // added to disable the SDK's LEGACY `client.trackEvents()` auto-fire
            // (V3 batch endpoint that 401s on storefront creds). Threading the
            // same flag through here disabled V4 clicks too on every storefront
            // hijack mount — LayoutRenderer forces `enableAnalytics: false` to
            // kill V3, and that suppressed all Object Clicked / Search Impression
            // emission. The buffer itself is null-safe: if no `<BufferProvider>`
            // is above OR `trackContext` is null, `clickBuffer.record(...)`
            // short-circuits silently (see createBufferTrio::click.record). So
            // the second gate is redundant AND prevents the legitimate V4 path
            // from firing. SCRUM-267 storefront-click regression.
            if (result.id) {
                try {
                    const results = resultsProp || stateResults;
                    const state = { currentPage, itemsPerPage: stateItemsPerPage };
                    const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
                    const searchContext = (results?.context ?? {});
                    const searchId = searchContext.searchId ??
                        searchContext.search_id;
                    const clickObjectData = {
                        object_type: "product",
                        ...(result.title ? { name: result.title } : {}),
                        ...(result.price ? { price: result.price } : {}),
                    };
                    if (searchId) {
                        clickBuffer.record(String(result.id), absolutePosition, searchId, clickObjectData, "results_page");
                    }
                    // Object Clicked Outside Search — non-search surfaces (collection /
                    // category) also fire the outside-search variant. Per the decision
                    // to duplicate on collection and dedupe downstream. SCRUM-267.
                    if (surfaceRef.current !== "search") {
                        clickBuffer.recordOutsideSearch(String(result.id), absolutePosition, clickObjectData, "shopify_collection");
                    }
                }
                catch (err) {
                    const error = err instanceof Error ? err : new Error(String(err));
                    log.error("SearchResults: Error recording click", {
                        resultId: result.id,
                        error: error.message,
                    });
                }
            }
            if (onResultClick) {
                onResultClick(result, index);
            }
        };
        const productUrl = toProductUrl(result.url);
        return (React.createElement("div", { key: result.id || index, id: `result-${index}`, role: "option", "data-result-index": index, "data-product-id": result.id, className: clsx("seekora-results__item", searchResultsTheme.resultItem, isActive && "seekora-results__item--active", isActive && searchResultsTheme.resultItemHover), onClick: handleClick, onMouseEnter: () => enableKeyboardNavigation && setActiveIndex(index), "aria-selected": isActive },
            result.image && (React.createElement("img", { src: result.image, alt: result.title, className: clsx("seekora-results__item-image", searchResultsTheme.resultImage) })),
            React.createElement("div", { className: "seekora-results__item-body" },
                React.createElement("h3", { className: clsx("seekora-results__item-title", searchResultsTheme.resultTitle) }, productUrl ? (React.createElement("a", { href: productUrl, className: "seekora-results__item-link", onClick: (e) => e.stopPropagation() }, result.title)) : (result.title)),
                showDescription && result.description && (React.createElement(DescriptionParagraph, { className: clsx("seekora-results__item-description", searchResultsTheme.resultDescription), html: result.description, asHtml: descriptionAsHtml, lineClamp: descriptionLines })),
                result.price && (React.createElement("div", { className: clsx("seekora-results__item-price", searchResultsTheme.resultPrice) }, result.price)))));
    };
    const defaultRenderResultCard = (result, index, isActive = false) => {
        const handleClick = () => {
            // Record V4 click into the batched buffer (search-attributed).
            // The buffer flushes ONE `Object Clicked` per (search_id, surface)
            // bucket via its idle/unload triggers — no per-click HTTP call.
            //
            // V4 buffer record is NOT gated on `enableAnalytics`. That flag was
            // added to disable the SDK's LEGACY `client.trackEvents()` auto-fire
            // (V3 batch endpoint that 401s on storefront creds). Threading the
            // same flag through here disabled V4 clicks too on every storefront
            // hijack mount — LayoutRenderer forces `enableAnalytics: false` to
            // kill V3, and that suppressed all Object Clicked / Search Impression
            // emission. The buffer itself is null-safe: if no `<BufferProvider>`
            // is above OR `trackContext` is null, `clickBuffer.record(...)`
            // short-circuits silently (see createBufferTrio::click.record). So
            // the second gate is redundant AND prevents the legitimate V4 path
            // from firing. SCRUM-267 storefront-click regression.
            if (result.id) {
                try {
                    const results = resultsProp || stateResults;
                    const state = { currentPage, itemsPerPage: stateItemsPerPage };
                    const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
                    const searchContext = (results?.context ?? {});
                    const searchId = searchContext.searchId ??
                        searchContext.search_id;
                    const clickObjectData = {
                        object_type: "product",
                        ...(result.title ? { name: result.title } : {}),
                        ...(result.price ? { price: result.price } : {}),
                    };
                    if (searchId) {
                        clickBuffer.record(String(result.id), absolutePosition, searchId, clickObjectData, "results_page");
                    }
                    // Object Clicked Outside Search — non-search surfaces (collection /
                    // category) also fire the outside-search variant. Per the decision
                    // to duplicate on collection and dedupe downstream. SCRUM-267.
                    if (surfaceRef.current !== "search") {
                        clickBuffer.recordOutsideSearch(String(result.id), absolutePosition, clickObjectData, "shopify_collection");
                    }
                }
                catch (err) {
                    const error = err instanceof Error ? err : new Error(String(err));
                    log.error("SearchResults: Error recording click", {
                        resultId: result.id,
                        error: error.message,
                    });
                }
            }
            if (onResultClick) {
                onResultClick(result, index);
            }
        };
        // Use widget view fields if available, otherwise fall back to regular fields
        const primaryText = result.primaryText || result.title;
        const secondaryText = result.secondaryText || result.description;
        const tertiaryText = result.tertiaryText || result.price;
        const imageUrl = result.imageUrl || result.image;
        const productUrl = toProductUrl(result.url);
        return (React.createElement("a", { key: result.id || index, id: `result-${index}`, role: "option", href: productUrl || "#", "data-result-index": index, "data-product-id": result.id, className: clsx("seekora-results__card", searchResultsTheme.resultItem, isActive && "seekora-results__card--active", isActive && searchResultsTheme.resultItemHover), onClick: (e) => {
                // Allow normal link navigation; still fire the click handler
                handleClick();
                if (!productUrl)
                    e.preventDefault();
            }, onMouseEnter: () => enableKeyboardNavigation && setActiveIndex(index), "aria-selected": isActive },
            imageUrl && (React.createElement("div", { className: "seekora-results__card-image-wrap" },
                React.createElement("img", { src: imageUrl, alt: primaryText, className: "seekora-results__card-image" }))),
            React.createElement("div", { className: "seekora-results__card-body" },
                primaryText && (React.createElement("h3", { className: clsx("seekora-results__card-title", searchResultsTheme.resultTitle) }, primaryText)),
                showDescription && secondaryText && (React.createElement(DescriptionParagraph, { className: clsx("seekora-results__card-description", searchResultsTheme.resultDescription), html: secondaryText, asHtml: descriptionAsHtml, lineClamp: descriptionLines })),
                tertiaryText && (React.createElement("div", { className: clsx("seekora-results__card-price", searchResultsTheme.resultPrice) }, tertiaryText)))));
    };
    const defaultRenderResult = viewMode === "card" || viewMode === "grid"
        ? defaultRenderResultCard
        : defaultRenderResultList;
    const defaultRenderEmpty = () => (React.createElement("div", { className: clsx("seekora-results__empty", searchResultsTheme.emptyState) }, "No results found"));
    const defaultRenderError = (err) => (React.createElement("div", { className: clsx("seekora-results__error", searchResultsTheme.errorState), role: "alert" },
        "Error: ",
        err.message));
    // Extract results array from response
    let rawResults = [];
    if (extractResults) {
        // Use custom extraction function if provided
        rawResults = extractResults(results) || [];
    }
    else if (results) {
        const res = results;
        // Post-processed shape (preferred — SeekoraClient already flattens data.results → results)
        if (Array.isArray(res.results) && res.results.length > 0) {
            rawResults = res.results;
        }
        else if (res.data?.results &&
            Array.isArray(res.data.results) &&
            res.data.results.length > 0) {
            // Raw envelope fallback: res.data.results
            rawResults = res.data.results;
        }
        else if (Array.isArray(results)) {
            rawResults = results;
        }
    }
    // Default field mapping (can be overridden via props)
    const defaultFieldMapping = {
        ...resolveFieldMapping(typeof fieldMapping === "string" ? fieldMapping : undefined),
        ...(typeof fieldMapping === "object" ? fieldMapping : {}),
    };
    // Extract field value using dot notation path
    const extractField = (item, path) => {
        if (!path)
            return undefined;
        return getNestedValue(item, path);
    };
    const resultItems = rawResults.map((item, index) => {
        try {
            // Widget view fields (when widget_mode is enabled) - check these first
            const primaryText = extractField(item, defaultFieldMapping.primaryText);
            const secondaryText = extractField(item, defaultFieldMapping.secondaryText);
            const tertiaryText = extractField(item, defaultFieldMapping.tertiaryText);
            const imageUrl = extractField(item, defaultFieldMapping.imageUrl);
            // Use primaryText as fallback for title if title is not found
            const title = extractField(item, defaultFieldMapping.title) ||
                primaryText ||
                "Untitled";
            const description = extractField(item, defaultFieldMapping.description) || secondaryText;
            const image = extractField(item, defaultFieldMapping.image) || imageUrl;
            const priceValue = extractField(item, defaultFieldMapping.price) || tertiaryText;
            const url = extractField(item, defaultFieldMapping.url);
            const id = extractField(item, defaultFieldMapping.id) || String(index);
            // Format price if it's a number
            const price = priceValue !== undefined ? formatPrice(priceValue) : undefined;
            // Extract custom fields if configured
            const customFields = {};
            if (defaultFieldMapping.custom) {
                Object.entries(defaultFieldMapping.custom).forEach(([key, path]) => {
                    customFields[key] = extractField(item, path);
                });
            }
            const resultItem = {
                id,
                title,
                description,
                url,
                image,
                price,
                metadata: item,
                ...customFields,
                // Widget view fields
                ...(primaryText && { primaryText }),
                ...(secondaryText && { secondaryText }),
                ...(tertiaryText && { tertiaryText }),
                ...(imageUrl && { imageUrl }),
                // Include original item for full access
                _raw: item,
            };
            return resultItem;
        }
        catch (err) {
            const error = err instanceof Error ? err : new Error(String(err));
            log.warn("SearchResults: Error extracting result item", {
                index,
                error: error.message,
                item: item?.id || "unknown",
            });
            // Return a fallback result item
            return {
                id: String(index),
                title: "Error loading result",
                metadata: item,
            };
        }
    });
    // Container style: always write CSS custom properties so the empty branch
    // also receives min-height/min-width and the container never collapses.
    // Consumer-supplied `style` overrides are spread last. SCRUM-268 fix #2.
    const containerStyle = {
        "--seekora-results-min-height": minHeight,
        "--seekora-results-min-width": minWidth,
        ...style,
    };
    // Log results extraction
    log.verbose("SearchResults: Extracted results", {
        rawResultsCount: rawResults.length,
        resultItemsCount: resultItems.length,
        viewMode,
        hasError: !!error,
        isLoading: loading,
    });
    if (error) {
        log.error("SearchResults: Rendering error state", {
            error: error.message,
            stack: error.stack,
        });
        return (React.createElement("div", { className: clsx("seekora-results", searchResultsTheme.container, className), "data-variant": viewMode, "data-state": "error", style: containerStyle }, renderError ? renderError(error) : defaultRenderError(error)));
    }
    if (!results || resultItems.length === 0) {
        log.verbose("SearchResults: No results to display");
        return (React.createElement("div", { className: clsx("seekora-results", searchResultsTheme.container, className), "data-variant": viewMode, "data-state": loading ? "loading" : "empty", style: containerStyle, "aria-busy": loading ? "true" : undefined }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
    }
    const renderFn = renderResult || defaultRenderResult;
    return (React.createElement("div", { ref: containerRef, className: clsx("seekora-results", searchResultsTheme.container, className), "data-variant": viewMode, style: containerStyle, tabIndex: enableKeyboardNavigation ? 0 : undefined, onKeyDown: handleKeyDown, role: "listbox", "aria-label": "Search results", "aria-activedescendant": activeIndex >= 0 ? `result-${activeIndex}` : undefined, "aria-busy": loading || undefined },
        showHeader &&
            (results?.totalResults !== undefined ||
                results?.data?.total_results !== undefined) &&
            resultItems.length > 0 && (React.createElement("div", { className: clsx("seekora-results__header", searchResultsTheme.header) },
            "Found ",
            results?.totalResults || results?.data?.total_results || 0,
            " ",
            "result",
            (results?.totalResults || results?.data?.total_results || 0) !== 1
                ? "s"
                : "")),
        cardMode === "theme_section" && cardSectionId ? (React.createElement(ThemeSectionGrid, { query: stateQuery || "", sectionId: cardSectionId, resultItems: resultItems, onResultClick: onResultClick, className: clsx("seekora-results__grid", searchResultsTheme.resultsList) })) : cardMode === "theme_native" &&
            themeHarvestTemplate &&
            themeHarvestSwapMap ? ((() => {
            const searchId = results?.context?.searchId ||
                results?.context?.search_id ||
                results?.data?.search_id ||
                "";
            // Phase C: the server supplied a harvested card template + swap map,
            // so use ThemeHarvestGrid (clone-and-swap). This renders the
            // merchant's ACTUAL theme card markup so native custom elements,
            // add-to-cart, and animations work on DOM insertion. SCRUM-267.
            //
            // Intentionally NOT gated on `shopifyAvailable`: ThemeHarvestGrid
            // renders by cloning raw HTML via DOMParser + swapCardHtml and never
            // touches hydrogen-react's ShopifyProvider hooks (the add-to-cart
            // path is the theme's own native <form action="/cart/add">, preserved
            // verbatim). Gating it on `shopifyAvailable` was over-restrictive: a
            // shop with no `storefrontToken` (so ShopifyContextBridge leaves the
            // ShopifyAvailableContext false) would silently fall back to generic
            // Seekora cards even with a valid harvested template + swap map.
            // SCRUM-267.
            return (React.createElement(ThemeHarvestGrid, { rawResults: rawResults, cardTemplateHtml: themeHarvestTemplate, swapMap: themeHarvestSwapMap, searchId: searchId, onResultClick: onResultClick, moneyFormat: themeHarvestMoneyFormat, className: clsx("seekora-results__grid", "seekora-results__grid--theme-harvest", searchResultsTheme.resultsList) }));
        })()) : (React.createElement("div", { className: clsx("seekora-results__grid", searchResultsTheme.resultsList) }, resultItems.map((result, index) => renderFn(result, index, index === activeIndex))))));
};
/**
 * `SearchResults` — public entrypoint. Wraps `SearchResultsInner` with
 * `<BufferProvider>` so the V4 batched-event buffers (Search Impression,
 * Object Clicked, Object Viewed) are available to every descendant card.
 *
 * Consumers don't need to wrap SearchResults in BufferProvider themselves —
 * the provider lives here at the component boundary. Advanced setups
 * that want a shared buffer across multiple SearchResults instances on
 * the same page can render their own `<BufferProvider>` above and
 * SearchResults' inner provider will just install a nested no-op
 * (each provider has its own buffer trio; nested providers do NOT share
 * — they are independent. That's the desired behaviour for now: one
 * `Search Impression` batch per SearchResults instance keeps the
 * search_id -> arrays correlation clean).
 */
export const SearchResults = (props) => (React.createElement(BufferProvider, null,
    React.createElement(SearchResultsInner, { ...props })));
