// ThemeHarvestGrid — renders Seekora search results using the merchant's own
// harvested card HTML (Phase C, SCRUM-267).
//
// Each result is a clone of cardTemplateHtml with product data swapped in via
// swapCardHtml(). Because the clone preserves the theme's custom elements
// (<product-card>, <quick-add-component>, <form action="/cart/add">), the
// theme's already-loaded JS upgrades them on DOM insertion — native
// add-to-cart, hover/zoom animations, and cart drawer all come for free.
//
// Analytics wiring mirrors ThemeSectionGrid:
//   - Delegated click on the container → Object Clicked via useObjectClickedBuffer.
//     Skipped for add-to-cart interactions (form[action*="/cart/add"],
//     quick-add-component, button[name="add"]).
//   - IntersectionObserver on each card (threshold 0.5, observe-once) →
//     Search Impression via useSearchImpressionBuffer.
import React, { useRef, useEffect, useCallback, useMemo } from "react";
import { useObjectClickedBuffer } from "../hooks/useObjectClickedBuffer";
import { useSearchImpressionBuffer } from "../hooks/useSearchImpressionBuffer";
import { useObjectViewedBuffer } from "../hooks/useObjectViewedBuffer";
import { useTrackContext } from "./SearchProvider";
import { trackProductListViewed } from "@seekora-ai/search-sdk";
import { toHydrogenProduct } from "../theme-cards/adapter";
import { swapCardHtml } from "../theme-cards/swap";
/**
 * Minimal seekora fallback card for a single result when swap throws.
 * Built with DOM APIs (no string concatenation) so handle/title values
 * containing `"` or `<` cannot inject markup or break attribute syntax.
 */
export function fallbackCardHtml(item, index) {
    try {
        const docData = item?.document || item || {};
        const title = String(docData.title || docData.name || `Result ${index + 1}`);
        const handle = String(docData.handle || "");
        const wrapper = document.createElement("div");
        wrapper.className = "seekora-results__card seekora-harvest-fallback";
        const link = document.createElement("a");
        // Property assignment — not string concat — so `"` in handle is safe.
        link.href = handle ? `/products/${handle}` : "#";
        // textContent cannot inject markup.
        link.textContent = title;
        wrapper.appendChild(link);
        return wrapper.outerHTML;
    }
    catch {
        return `<div class="seekora-results__card seekora-harvest-fallback">Product</div>`;
    }
}
export const ThemeHarvestGrid = ({ rawResults, cardTemplateHtml, swapMap, searchId, onResultClick, className, moneyFormat, }) => {
    const containerRef = useRef(null);
    const clickBuffer = useObjectClickedBuffer();
    const impressionBuffer = useSearchImpressionBuffer();
    const viewedBuffer = useObjectViewedBuffer();
    const trackCtx = useTrackContext();
    // Surface ("search" | "collection" | ...) from the layout's
    // data-seekora-surface ancestor. Drives the non-search events
    // (Product List Viewed + Object Clicked Outside Search). SCRUM-267.
    const surfaceRef = useRef("search");
    const plvFiredRef = useRef("");
    // Pre-compute swapped HTML for all results. Memoized so unrelated parent
    // re-renders do not re-parse the template N times or flush dangerouslySetInnerHTML
    // (which would reset live custom-element state, e.g. an open quick-add drawer).
    const swappedHtmls = useMemo(() => rawResults.map((item, i) => {
        try {
            const product = toHydrogenProduct(item);
            return swapCardHtml(cardTemplateHtml, swapMap, product, moneyFormat);
        }
        catch {
            return fallbackCardHtml(item, i);
        }
    }), [rawResults, cardTemplateHtml, swapMap, moneyFormat]);
    // IntersectionObserver — Search Impression on first viewport entry.
    useEffect(() => {
        const container = containerRef.current;
        if (!container || rawResults.length === 0 || !searchId)
            return;
        // Cache the surface so the click handler + PLV effect can branch.
        const host = container.closest("[data-seekora-surface]");
        const s = host?.getAttribute("data-seekora-surface");
        if (s)
            surfaceRef.current = s;
        const cards = container.querySelectorAll("[data-seekora-theme-card]");
        if (cards.length === 0)
            return;
        const observer = new IntersectionObserver((entries) => {
            for (const entry of entries) {
                if (!entry.isIntersecting)
                    continue;
                const el = entry.target;
                const idx = Number(el.dataset.seekoraIndex);
                const item = rawResults[idx];
                if (item) {
                    const oid = String(item?.id ?? item?.document?.id ?? idx);
                    const odata = {
                        object_type: "product",
                        ...(item?.document?.title || item?.title
                            ? { name: item?.document?.title || item?.title }
                            : {}),
                    };
                    impressionBuffer.record(oid, idx + 1, searchId, odata, "results_page");
                    // Object Viewed — universal item impression alongside Search
                    // Impression on every card surface (search + collection). SCRUM-267.
                    viewedBuffer.record(oid, idx + 1, odata, "results_page");
                }
                observer.unobserve(entry.target);
            }
        }, { threshold: 0.5 });
        cards.forEach((c) => observer.observe(c));
        return () => observer.disconnect();
        // rawResults is a new array identity per search — correct invalidation signal.
        // Using .length would miss same-count pagination/filter changes.
        // impressionBuffer is stable (ref-based); omitted intentionally.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [rawResults, searchId]);
    // V4 "Product List Viewed" (event 17) — ONE emit per non-search results
    // grid (collection / category). The search-page twin is Search Submitted/
    // Impression; this is the non-search list-view event. SCRUM-267.
    useEffect(() => {
        const host = containerRef.current?.closest("[data-seekora-surface]");
        const surface = host?.getAttribute("data-seekora-surface") || "search";
        if (surface === "search")
            return;
        if (rawResults.length === 0)
            return;
        const ids = rawResults
            .map((it) => String(it?.id ?? it?.document?.id ?? ""))
            .filter(Boolean);
        if (ids.length === 0)
            return;
        const fingerprint = `${surface}:${ids.join(",")}`;
        if (plvFiredRef.current === fingerprint)
            return;
        plvFiredRef.current = fingerprint;
        if (!trackCtx)
            return;
        try {
            trackProductListViewed(trackCtx, {
                object_ids: ids,
                object_positions: ids.map((_, i) => i + 1),
                object_data: rawResults.map((it) => ({
                    object_type: "product",
                    ...(it?.document?.title || it?.title
                        ? { name: it?.document?.title || it?.title }
                        : {}),
                })),
                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
    }, [rawResults]);
    const handleContainerClick = useCallback((e) => {
        // Skip add-to-cart interactions — let the theme's native form/quick-add
        // handle them. We only record Object Clicked for product navigation.
        const target = e.target;
        if (target.closest('form[action*="/cart/add"]') ||
            target.closest("quick-add-component") ||
            target.closest('button[name="add"]')) {
            return;
        }
        const card = target.closest("[data-seekora-theme-card]");
        if (!card)
            return;
        const idx = Number(card.dataset.seekoraIndex);
        const item = rawResults[idx];
        if (!item)
            return;
        const oid = String(item?.id ?? item?.document?.id ?? idx);
        const odata = {
            object_type: "product",
            ...(item?.document?.title || item?.title
                ? { name: item?.document?.title || item?.title }
                : {}),
        };
        clickBuffer.record(oid, idx + 1, searchId, odata, "results_page");
        // Object Clicked Outside Search — non-search surfaces (collection /
        // category) also fire the outside-search variant. Dupe on collection,
        // dedupe downstream. SCRUM-267.
        if (surfaceRef.current !== "search") {
            clickBuffer.recordOutsideSearch(oid, idx + 1, odata, "shopify_collection");
        }
        if (onResultClick)
            onResultClick(item, idx);
    }, [rawResults, clickBuffer, searchId, onResultClick]);
    return (React.createElement("div", { ref: containerRef, className: className, onClick: handleContainerClick }, swappedHtmls.map((html, i) => (React.createElement("div", { key: i, "data-seekora-theme-card": "", "data-seekora-index": i, 
        // eslint-disable-next-line react/no-danger
        dangerouslySetInnerHTML: { __html: html } })))));
};
