/**
 * useVirtualGrid.js - unified virtualization module for the Vue migration.
 *
 * This centralizes virtualization helpers that were previously split
 * across VirtualScroller.js and InfiniteScroll.js.
 */
import { getFilenameKey, shouldHideSiblingAsset, unregisterHiddenSibling } from "../../features/grid/AssetCardRenderer.js";
import {
    gridQueryHasActiveFilters,
    readGridQueryFromDataset,
} from "../grid/useGridQuery.js";
import { syncAssetCollectionState } from "../grid/useAssetCollection.js";
import { resolvePageAdvance } from "../grid/usePagedAssets.js";

export function isPotentialScrollContainer(el: any) {
    if (!el || el === window) return false;
    if (el === document.body || el === document.documentElement) return false;
    try {
        const style = window.getComputedStyle(el);
        const overflowY = String(style?.overflowY || "");
        if (!/(auto|scroll|overlay)/.test(overflowY)) return false;
        const clientH = Number(el.clientHeight) || 0;
        if (clientH <= 0) return false;
        return true;
    } catch {
        return false;
    }
}

export function detectScrollRoot(gridContainer: any) {
    try {
        const browse = gridContainer?.closest?.(".mjr-am-browse") || null;
        if (browse && isPotentialScrollContainer(browse)) return browse;
    } catch (e) {
        console.debug?.(e);
    }
    try {
        let cur = gridContainer?.parentElement;
        while (cur && cur !== document.body && cur !== document.documentElement) {
            if (isPotentialScrollContainer(cur)) return cur;
            cur = cur.parentElement;
        }
    } catch (e) {
        console.debug?.(e);
    }
    return gridContainer?.parentElement || null;
}

export function getScrollContainer(gridContainer: any, state: any) {
    try {
        const cur = state?.scrollRoot;
        if (cur && cur instanceof HTMLElement) return cur;
    } catch (e) {
        console.debug?.(e);
    }
    const detected = detectScrollRoot(gridContainer);
    if (detected && state) state.scrollRoot = detected;
    return detected;
}

export function ensureVirtualGrid(gridContainer: any, state: any, deps: any) {
    if (state.virtualGrid) {
        // Guard: a disposed instance has its DOM detached; discard it so a fresh one is created.
        if (state.virtualGrid._disposed) {
            state.virtualGrid = null;
        } else {
            return state.virtualGrid;
        }
    }
    const scrollRoot = getScrollContainer(gridContainer, state);
    try {
        deps.gridDebug("virtualGrid:scrollRoot", {
            scrollRoot:
                scrollRoot === document.body
                    ? "document.body"
                    : scrollRoot === document.documentElement
                      ? "document.documentElement"
                      : scrollRoot?.className || scrollRoot?.tagName || null,
        });
    } catch (e) {
        console.debug?.(e);
    }
    state.virtualGrid = new deps.VirtualGrid(gridContainer, scrollRoot, deps.optionsFactory());
    if (!state._cardKeydownHandler) {
        const handler = (event: any) => {
            try {
                if (!event?.key) return;
                if (event.key !== "Enter" && event.key !== " ") return;
                const card = event.target?.closest?.(".mjr-asset-card");
                if (!card) return;
                event.preventDefault();
                card.click();
            } catch (e) {
                console.debug?.(e);
            }
        };
        state._cardKeydownHandler = handler;
        gridContainer.addEventListener("keydown", handler, true);
    }
    return state.virtualGrid;
}

export function ensureSentinel(gridContainer: any, state: any, sentinelClass: any) {
    let sentinel = state.sentinel;
    if (
        sentinel &&
        sentinel.isConnected &&
        sentinel.parentNode === gridContainer &&
        !sentinel.nextSibling
    )
        return sentinel;
    if (sentinel) {
        sentinel.remove();
    } else {
        sentinel = document.createElement("div");
        sentinel.className = sentinelClass;
        sentinel.style.cssText =
            "height: 1px; width: 100%; position: absolute; bottom: 0; left: 0; pointer-events: none; z-index: -10;";
        state.sentinel = sentinel;
    }
    gridContainer.appendChild(sentinel);
    if (state.observer) {
        try {
            state.observer.observe(sentinel);
        } catch (e) {
            console.debug?.(e);
        }
    }
    return sentinel;
}

export function stopObserver(state: any, gridContainer: any = null) {
    try {
        if (state.observer) state.observer.disconnect();
    } catch (e) {
        console.debug?.(e);
    }
    state.observer = null;
    try {
        if (state.sentinel && state.sentinel.isConnected) state.sentinel.remove();
    } catch (e) {
        console.debug?.(e);
    }
    state.sentinel = null;
    try {
        if (state.scrollTarget && state.scrollHandler) {
            state.scrollTarget.removeEventListener("scroll", state.scrollHandler);
        }
    } catch (e) {
        console.debug?.(e);
    }
    state.scrollRoot = null;
    state.scrollTarget = null;
    state.scrollHandler = null;
    state.ignoreNextScroll = false;
    state.userScrolled = false;
    state.allowUntilFilled = true;
    if (gridContainer && state._cardKeydownHandler) {
        try {
            gridContainer.removeEventListener("keydown", state._cardKeydownHandler, true);
        } catch (e) {
            console.debug?.(e);
        }
        state._cardKeydownHandler = null;
    }
}

export function captureScrollMetrics(state: any) {
    const root = state?.scrollRoot;
    if (!root) return null;
    try {
        if (!root.isConnected) return null;
    } catch (e) {
        console.debug?.(e);
    }
    const clientHeight = Number(root.clientHeight) || 0;
    if (clientHeight <= 0) return null;
    const scrollHeight = Number(root.scrollHeight) || 0;
    const scrollTop = Number(root.scrollTop) || 0;
    const bottomGap = scrollHeight - (scrollTop + clientHeight);
    return { clientHeight, scrollHeight, scrollTop, bottomGap };
}

export function maybeKeepPinnedToBottom(_state: any, _before: any) {
    return;
}

export function resolvePageAdvanceCount({ count = 0, limit = 0, offset = 0, total = null as number | null } = {}) {
    return resolvePageAdvance({ count, limit, offset, total });
}

export function startInfiniteScroll(gridContainer: any, state: any, deps: any) {
    if (!deps.config.INFINITE_SCROLL_ENABLED) return;
    stopObserver(state);
    const sentinel = ensureSentinel(gridContainer, state, deps.sentinelClass);
    let rootEl;
    try {
        rootEl = state?.virtualGrid?.scrollElement || null;
    } catch {
        rootEl = null;
    }
    if (!rootEl) rootEl = getScrollContainer(gridContainer, state);
    if (!rootEl || !(rootEl instanceof HTMLElement)) {
        deps.gridDebug("infiniteScroll:disabled", { reason: "no scroll container" });
        return;
    }
    state.scrollRoot = rootEl;
    state.userScrolled = false;
    const scrollTarget = rootEl;
    state.scrollTarget = scrollTarget;
    deps.gridDebug("infiniteScroll:setup", {
        rootEl: rootEl?.className || rootEl?.tagName || null,
        scrollTarget: scrollTarget?.className || scrollTarget?.tagName || null,
        rootMargin: deps.config.INFINITE_SCROLL_ROOT_MARGIN || "800px",
        threshold: deps.config.INFINITE_SCROLL_THRESHOLD ?? 0.01,
        offset: Number(state?.offset || 0) || 0,
        done: !!state?.done,
    });
    if (scrollTarget && !state.scrollHandler) {
        state.scrollHandler = () => {
            if (state.ignoreNextScroll) {
                state.ignoreNextScroll = false;
                return;
            }
            state.userScrolled = true;
            try {
                if (state.loading || state.done || state.allowUntilFilled) return;
                const m = captureScrollMetrics(state);
                const bottomGapPx = Math.max(0, Number(deps.config.BOTTOM_GAP_PX || 80));
                if (m && m.bottomGap <= bottomGapPx) {
                    Promise.resolve(deps.loadNextPage(gridContainer, state)).catch(() => null);
                }
            } catch (e) {
                console.debug?.(e);
            }
        };
        try {
            scrollTarget.addEventListener("scroll", state.scrollHandler, { passive: true });
        } catch (e) {
            console.debug?.(e);
        }
    }
    const observerRoot = rootEl;
    state.observer = new IntersectionObserver(
        (entries) => {
            for (const entry of entries || []) {
                if (!entry.isIntersecting) continue;
                if (state.loading || state.done) return;
                const metrics = captureScrollMetrics(state);
                if (!metrics) return;
                const fillsViewport = metrics
                    ? metrics.scrollHeight > metrics.clientHeight + 40
                    : false;
                if (!state.userScrolled && fillsViewport && !state.allowUntilFilled) return;
                try {
                    state.observer?.unobserve?.(sentinel);
                } catch (e) {
                    console.debug?.(e);
                }
                state.userScrolled = false;
                if (fillsViewport) state.allowUntilFilled = false;
                Promise.resolve(deps.loadNextPage(gridContainer, state))
                    .catch(() => null)
                    .finally(() => {
                        if (state.done) return;
                        if (!sentinel.isConnected) return;
                        try {
                            state.observer?.observe?.(sentinel);
                        } catch (e) {
                            console.debug?.(e);
                        }
                    });
            }
        },
        {
            root: observerRoot,
            rootMargin: deps.config.INFINITE_SCROLL_ROOT_MARGIN || "800px",
            threshold: deps.config.INFINITE_SCROLL_THRESHOLD ?? 0.01,
        },
    );
    state.observer.observe(sentinel);
}


function _assetMatchesActiveFilters(gridContainer: any, asset: any) {
    if (!asset || typeof asset !== "object") return false;
    const query = readGridQueryFromDataset(gridContainer?.dataset || {});
    const scope = query.scope;
    const subfolder = query.subfolder.toLowerCase();
    const kind = query.kind;
    const workflowOnly = query.workflowOnly;
    const minRating = query.minRating;
    const workflowType = query.workflowType.toLowerCase();
    const workflowId = String(query.workflowId || "").trim();
    const dateExact = query.dateExact;
    const assetSubfolder = String(asset?.subfolder || "").trim().toLowerCase();
    const assetType = String(asset?.type || asset?.source || "output")
        .trim()
        .toLowerCase();
    const assetKind = String(asset?.kind || "").trim().toLowerCase();
    const assetWorkflowType = String(asset?.workflow_type || asset?.workflowType || "")
        .trim()
        .toLowerCase();
    const assetWorkflowId = String(
        asset?.workflow_id ||
            asset?.workflowId ||
            asset?.metadata?.workflow_id ||
            asset?.metadata?.workflow?.id ||
            "",
    ).trim();
    const assetDate = String(asset?.date_exact || asset?.date || "").trim();
    const hasWorkflow = asset?.has_workflow ?? asset?.hasWorkflow ?? null;

    if (scope && scope !== "all" && assetType && assetType !== scope) return false;
    if (subfolder && assetSubfolder !== subfolder) return false;
    if (kind && assetKind && assetKind !== kind) return false;
    // Treat null/undefined has_workflow as "pending" (enrichment in progress)
    // so freshly generated assets are not rejected by the workflow-only filter.
    // Only reject when has_workflow is explicitly false/0.
    if (workflowOnly && hasWorkflow !== null && !hasWorkflow) return false;
    if (minRating > 0 && (Number(asset?.rating || 0) || 0) < minRating) return false;
    if (workflowType && assetWorkflowType !== workflowType) return false;
    if (workflowId && assetWorkflowId && assetWorkflowId !== workflowId) return false;
    if (dateExact && assetDate && assetDate !== dateExact) return false;

    return true;
}

export async function fetchPage(
    gridContainer: any,
    query: any,
    limit: any,
    offset: any,
    deps: any,
    { requestId = 0, signal = null, cursor = null } = {},
) {
    const coerceQueryText = (value: any) => {
        if (typeof value === "string") return value;
        if (value && typeof value === "object") {
            if (typeof value.value === "string") return value.value;
            if (typeof value.target?.value === "string") return value.target.value;
        }
        return String(value || "");
    };

    const queryState = readGridQueryFromDataset(gridContainer?.dataset || {});
    const {
        scope,
        customRootId,
        subfolder,
        kind,
        workflowOnly,
        minRating,
        minSizeMB,
        maxSizeMB,
        resolutionCompare,
        minWidth,
        minHeight,
        maxWidth,
        maxHeight,
        workflowType,
        workflowId,
        workflowModel,
        workflowRunsOn,
        dateRange,
        dateExact,
        metadataSearchMode,
    } = queryState;
    const sortKey = queryState.sort || "mtime_desc";
    const requestedQueryRaw = coerceQueryText(query).trim();
    const requestedQuery = requestedQueryRaw || "*";
    const normalizedRequestedQuery = /^\[object\s+HTML.*Element\]$/i.test(requestedQuery)
        ? "*"
        : requestedQuery;
    const safeQuery = deps.sanitizeQuery(normalizedRequestedQuery) || normalizedRequestedQuery;
    try {
        const isOutputScope = String(scope || "").toLowerCase() === "output";
        const hasActiveFilters = gridQueryHasActiveFilters({
            ...queryState,
            q: safeQuery,
        });
        const isDefaultOutputBrowse =
            isOutputScope &&
            Number(offset ?? 0) === 0 &&
            safeQuery === "*" &&
            !hasActiveFilters &&
            String(sortKey || "mtime_desc").toLowerCase() === "mtime_desc";
        const includeTotal = !(isOutputScope && (Number(offset ?? 0) > 0 || isDefaultOutputBrowse));
        const groupStacksForRequest = !!queryState.groupStacks;
        const cursorForRequest = Number(offset ?? 0) > 0 ? null : cursor || null;
        const url = deps.buildListURL({
            q: safeQuery,
            limit,
            offset,
            scope,
            subfolder,
            customRootId: customRootId || null,
            kind: kind || null,
            hasWorkflow: workflowOnly ? true : null,
            minRating: minRating > 0 ? minRating : null,
            minSizeMB: minSizeMB > 0 ? minSizeMB : null,
            maxSizeMB: maxSizeMB > 0 ? maxSizeMB : null,
            resolutionCompare,
            minWidth: minWidth > 0 ? minWidth : null,
            minHeight: minHeight > 0 ? minHeight : null,
            maxWidth: maxWidth > 0 ? maxWidth : null,
            maxHeight: maxHeight > 0 ? maxHeight : null,
            workflowType: workflowType || null,
            workflowId: workflowId || null,
            workflowModel: workflowModel || null,
            runsOn: workflowRunsOn || null,
            dateRange: dateRange || null,
            dateExact: dateExact || null,
            sort: sortKey,
            cursor: cursorForRequest,
            includeTotal,
            groupStacks: groupStacksForRequest,
            metadataMode: metadataSearchMode || null,
        });
        const result = await deps.get(url, { timeoutMs: 120_000, ...(signal ? { signal } : {}) });
        try {
            const state = deps.getGridState(gridContainer);
            if (state && Number(state.requestId) !== Number(requestId)) {
                return { ok: false, stale: true, error: "Stale response" };
            }
        } catch (e) {
            console.debug?.(e);
        }
        if (result.ok) {
            const assets = result.data?.assets || [];
            const serverCount = Array.isArray(assets) ? assets.length : 0;
            const rawTotal = result.data?.total;
            const total = rawTotal == null ? null : Number(rawTotal ?? 0) || 0;
            const responseLimit = Math.max(0, Number(result.data?.limit) || 0);
            const responseOffset = Math.max(0, Number(result.data?.offset) || 0);
            const nextCursor = result.data?.next_cursor || result.data?.nextCursor || null;
            const hasMore = typeof result.data?.has_more === "boolean" ? result.data.has_more : null;
            return {
                ok: true,
                assets,
                total,
                count: serverCount,
                limit: responseLimit,
                offset: responseOffset,
                nextCursor,
                hasMore,
                sortKey,
                safeQuery,
            };
        }
        try {
            if (String(result?.code || "") === "ABORTED")
                return { ok: false, aborted: true, error: "Aborted" };
        } catch (e) {
            console.debug?.(e);
        }
        return { ok: false, error: result.error };
    } catch (error) {
        try {
            if (String((error as any)?.name || "") === "AbortError")
                return { ok: false, aborted: true, error: "Aborted" };
        } catch (e) {
            console.debug?.(e);
        }
        return { ok: false, error: (error as any).message };
    }
}

export function emitAgendaStatus(dateExact: any, hasResults: any) {
    if (!dateExact) return;
    try {
        window?.dispatchEvent?.(
            new CustomEvent("MJR:AgendaStatus", {
                detail: { date: dateExact, hasResults: Boolean(hasResults) },
            }),
        );
    } catch (e) {
        console.debug?.(e);
    }
}

export async function loadNextPage(gridContainer: any, state: any, deps: any) {
    if (state.loading || state.done) return;
    const limit = Math.max(1, Math.min(deps.config.MAX_PAGE_SIZE, deps.config.DEFAULT_PAGE_SIZE));
    state.loading = true;
    const before = deps.captureScrollMetrics(state);
    deps.gridDebug("loadNextPage:start", {
        q: String(state?.query || ""),
        limit,
        offset: Number(state?.offset ?? 0) || 0,
    });
    try {
        const page = await fetchPage(gridContainer, state.query, limit, state.offset, deps, {
            requestId: Number(state.requestId ?? 0) || 0,
            signal: state.abortController?.signal || null,
        });
        const dateExact = readGridQueryFromDataset(gridContainer?.dataset || {}).dateExact;
        emitAgendaStatus(
            dateExact,
            page.ok && Array.isArray(page.assets) && page.assets.length > 0,
        );
        if (!page.ok) {
            if (page.aborted || page.stale) return;
            state.done = true;
            deps.stopObserver(state);
            return;
        }
        if (page.total != null) state.total = page.total;
        const added = deps.appendAssets(gridContainer, page.assets || [], state);
        const consumedCount = resolvePageAdvanceCount({
            count: page.count,
            limit: page.limit || limit,
            offset: state.offset,
            total: page.total != null ? page.total : state.total,
        });
        state.offset += consumedCount;
        deps.gridDebug("loadNextPage:append", { added, offset: state.offset });
        deps.maybeKeepPinnedToBottom(state, before);
        const knownTotal = Number(state.total);
        if (
            consumedCount <= 0 ||
            (Number.isFinite(knownTotal) && knownTotal > 0 && state.offset >= knownTotal)
        ) {
            state.done = true;
            deps.stopObserver(state);
        }
    } finally {
        state.loading = false;
    }
}

export function getSortValue(asset: any, sortKey: any) {
    switch (sortKey) {
        case "mtime_desc":
            // Negate so that larger mtime (newer) sorts first in ascending numeric order.
            return -(Number(asset?.mtime) || 0);
        case "mtime_asc":
            return Number(asset?.mtime) || 0;
        case "name_asc":
        case "name_desc":
            return String(asset?.filename || "").toLowerCase();
        default:
            // Default to newest-first (descending mtime) via negation.
            return -(Number(asset?.mtime) || 0);
    }
}

export function compareAssets(a: any, b: any, sortKey: any) {
    if (sortKey === "name_desc") {
        // Reverse the natural string order for descending name sort.
        const av = String(a?.filename || "").toLowerCase();
        const bv = String(b?.filename || "").toLowerCase();
        if (av > bv) return -1;
        if (av < bv) return 1;
        return 0;
    }
    // For mtime sorts, getSortValue already negates desc values,
    // so a plain ascending compare yields the correct order.
    const av = getSortValue(a, sortKey);
    const bv = getSortValue(b, sortKey);
    if (av < bv) return -1;
    if (av > bv) return 1;
    return 0;
}

export function shouldInsertBefore(assetValue: any, cardValue: any, sortKey: any) {
    if (sortKey === "name_asc") return String(assetValue) < String(cardValue);
    if (sortKey === "name_desc") return String(assetValue) > String(cardValue);
    return Number(assetValue) < Number(cardValue);
}

export function findInsertPosition(array: any, asset: any, sortKey: any) {
    const assetValue = getSortValue(asset, sortKey);
    for (let i = 0; i < array.length; i++) {
        const arrValue = getSortValue(array[i], sortKey);
        if (shouldInsertBefore(assetValue, arrValue, sortKey)) return i;
    }
    return array.length;
}

export function findAssetElement(gridContainer: any, assetId: any) {
    const escaped = _safeEscape(String(assetId));
    return gridContainer.querySelector(`[data-mjr-asset-id="${escaped}"]`);
}

function _safeEscape(value: any) {
    try {
        return CSS?.escape
            ? CSS.escape(value)
            : value.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1");
    } catch {
        return value.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1");
    }
}

export function getUpsertBatchState(gridContainer: any, upsertState: any) {
    let s = upsertState.get(gridContainer);
    if (!s) {
        s = { pending: new Map<string, any>(), timer: null, flushing: false };
        upsertState.set(gridContainer, s);
    }
    return s;
}

function _normalizeAssetIdentityPart(value: any) {
    return String(value ?? "")
        .trim()
        .toLowerCase();
}

function _isLivePlaceholderAsset(asset: any) {
    return (
        asset?._mjrLivePlaceholder === true ||
        asset?.is_live_placeholder === true ||
        String(asset?.id || "")
            .trim()
            .toLowerCase()
            .startsWith("live:")
    );
}

function _clearLivePlaceholderState(asset: any) {
    if (!asset || typeof asset !== "object") return asset;
    try {
        delete asset._mjrLivePlaceholder;
        delete asset._mjrLiveStatus;
        delete asset._mjrLiveLabel;
        delete asset.is_live_placeholder;
    } catch (e) {
        console.debug?.(e);
    }
    return asset;
}

function _getAssetIdentityKey(asset: any) {
    if (!asset || typeof asset !== "object") return "";
    const type = _normalizeAssetIdentityPart(asset?.type || asset?.source || "output");
    const rootId = _normalizeAssetIdentityPart(asset?.root_id || asset?.custom_root_id || "");
    const subfolder = _normalizeAssetIdentityPart(asset?.subfolder || "");
    const filename = _normalizeAssetIdentityPart(asset?.filename || "");
    if (filename) return `${type}|${rootId}|${subfolder}|${filename}`;
    const filepath = _normalizeAssetIdentityPart(
        asset?.filepath || asset?.path || asset?.fullpath || asset?.full_path || "",
    );
    if (!filepath) return "";
    return `${type}|${rootId}|path|${filepath}`;
}

function _findExistingAssetIndex(state: any, assetId: any, candidateAsset: any) {
    const list = Array.isArray(state?.assets) ? state.assets : [];
    if (assetId) {
        const exactIndex = list.findIndex((asset: any) => String(asset?.id || "") === assetId);
        if (exactIndex > -1) return exactIndex;
    }
    const identityKey = _getAssetIdentityKey(candidateAsset);
    if (!identityKey) return -1;
    return list.findIndex((asset: any) => _getAssetIdentityKey(asset) === identityKey);
}

function _shouldKeepExistingAssetOnFilterMismatch(incomingAsset: any, existingAsset: any) {
    if (!existingAsset || typeof existingAsset !== "object") return false;
    if (!incomingAsset || typeof incomingAsset !== "object") return false;
    if (_isLivePlaceholderAsset(existingAsset)) return true;
    const protectedFields = [
        "filename",
        "filepath",
        "path",
        "fullpath",
        "full_path",
        "subfolder",
        "type",
        "source",
        "root_id",
        "custom_root_id",
        "kind",
        "rating",
        "workflow_type",
        "workflowType",
        "date_exact",
        "date",
    ];
    return !protectedFields.some((field) => Object.prototype.hasOwnProperty.call(incomingAsset, field));
}

function dedupeAssetsByKey(state: any, deps: any) {
    const seenIds = new Set();
    const seenKeys = new Set();
    const filenamePrimary = new Map();
    const deduped: any[] = [];
    for (const asset of Array.isArray(state?.assets) ? state.assets : []) {
        const assetId = asset?.id != null ? String(asset.id) : "";
        const key = deps.assetKey(asset);
        const filenameKey = getFilenameKey(asset?.filename);
        if (assetId && seenIds.has(assetId)) continue;
        if (key && seenKeys.has(key)) continue;

        if (filenameKey) {
            const primary = filenamePrimary.get(filenameKey);
            if (primary) {
                const prevMembers = Array.isArray(primary._mjrDupMembers) ? primary._mjrDupMembers : [primary];
                const prevIds = new Set(prevMembers.map((a: any) => String(a?.id || "")));
                const merged = [
                    ...prevMembers,
                    ...[asset].filter((a) => !prevIds.has(String(a?.id || ""))),
                ];
                primary._mjrDupStack = true;
                primary._mjrDupMembers = merged;
                primary._mjrDupCount = merged.length;
                primary._mjrNameCollision = false;
                delete primary._mjrNameCollisionCount;
                delete primary._mjrNameCollisionPaths;

                asset._mjrNameCollision = false;
                delete asset._mjrNameCollisionCount;
                delete asset._mjrNameCollisionPaths;
                asset._mjrDupStack = false;
                asset._mjrDupMembers = null;
                asset._mjrDupCount = 0;
                continue;
            }
        }

        if (assetId) seenIds.add(assetId);
        if (key) seenKeys.add(key);
        if (filenameKey) filenamePrimary.set(filenameKey, asset);

        asset._mjrNameCollision = false;
        delete asset._mjrNameCollisionCount;
        delete asset._mjrNameCollisionPaths;
        deduped.push(asset);
    }
    state.assets = deduped;
    syncAssetCollectionState(state, { assetKey: deps.assetKey });
}

/**
 * Flush all pending upsert operations into the grid.
 *
 * Concurrency guard: `batchState.flushing` prevents re-entrant flushes.
 * Items arriving during a flush are accumulated in `batchState.pending` and
 * a follow-up flush is scheduled in the `finally` block (BUG-01).
 */
export function flushUpsertBatch(gridContainer: any, deps: any) {
    const batchState = deps.upsertState.get(gridContainer) as any;
    if (!batchState || batchState.pending.size === 0 || batchState.flushing) return;
    batchState.flushing = true;
    if (batchState.timer) {
        clearTimeout(batchState.timer);
        batchState.timer = null;
    }
    // Snapshot the current batch so items arriving during the flush are not lost (BUG-01).
    const snapshot = new Map(batchState.pending) as Map<string, any>;
    for (const key of snapshot.keys()) batchState.pending.delete(key);
    const state = deps.getOrCreateState(gridContainer);
    const vg = deps.ensureVirtualGrid(gridContainer, state);
    try {
        let modified = false;
        for (const [assetId, asset] of snapshot.entries()) {
            const incomingIsPlaceholder = _isLivePlaceholderAsset(asset);
            state.assetKeyFn = deps.assetKey;
            const siblingCheck = shouldHideSiblingAsset(asset, state, deps.loadMajoorSettings);
            if (Array.isArray(siblingCheck?.removed) && siblingCheck.removed.length) {
                const removedSet = new Set(siblingCheck.removed);
                state.assets = state.assets.filter((item: any) => {
                    const keep = !removedSet.has(item);
                    if (!keep) {
                        unregisterHiddenSibling(state, item, state);
                    }
                    return keep;
                });
                try {
                    state.hiddenPngSiblings =
                        (Number(state.hiddenPngSiblings || 0) || 0) + siblingCheck.removed.length;
                } catch (e) {
                    console.debug?.(e);
                }
                modified = true;
            }
            if (siblingCheck?.hidden) {
                try {
                    state.hiddenPngSiblings = (Number(state.hiddenPngSiblings || 0) || 0) + 1;
                } catch (e) {
                    console.debug?.(e);
                }
                continue;
            }
            const existingIndex = state.assets.findIndex((a: any) => String(a.id) === assetId);
            const existingAsset = existingIndex > -1 ? state.assets[existingIndex] : null;
            const candidateAsset = existingAsset ? { ...existingAsset, ...asset } : asset;
            const resolvedExistingIndex =
                existingIndex > -1
                    ? existingIndex
                    : _findExistingAssetIndex(state, assetId, candidateAsset);
            const resolvedExistingAsset =
                resolvedExistingIndex > -1 ? state.assets[resolvedExistingIndex] : null;
            const mergedCandidate = resolvedExistingAsset
                ? { ...resolvedExistingAsset, ...asset }
                : candidateAsset;
            const key = deps.assetKey(candidateAsset);
            const matchesFilters = _assetMatchesActiveFilters(gridContainer, mergedCandidate);
            if (!matchesFilters) {
                if (resolvedExistingIndex > -1) {
                    if (_shouldKeepExistingAssetOnFilterMismatch(asset, resolvedExistingAsset)) {
                        Object.assign(resolvedExistingAsset, asset);
                        if (!incomingIsPlaceholder) {
                            _clearLivePlaceholderState(resolvedExistingAsset);
                        }
                        state.assets[resolvedExistingIndex] = { ...resolvedExistingAsset };
                        modified = true;
                        continue;
                    }
                    const [removedAsset] = state.assets.splice(resolvedExistingIndex, 1);
                    unregisterHiddenSibling(state, removedAsset, state);
                    modified = true;
                }
                continue;
            }
            if (resolvedExistingIndex > -1) {
                const previousKey = deps.assetKey(resolvedExistingAsset);
                Object.assign(resolvedExistingAsset, asset);
                if (!incomingIsPlaceholder) {
                    _clearLivePlaceholderState(resolvedExistingAsset);
                }
                const mergedAsset = { ...resolvedExistingAsset };
                const nextKey = deps.assetKey(mergedAsset);
                state.assets[resolvedExistingIndex] = mergedAsset;
                if (previousKey && previousKey !== nextKey) {
                    state.seenKeys?.delete?.(previousKey);
                    if (nextKey) state.seenKeys?.add?.(nextKey);
                }
                modified = true;
            } else {
                const alreadySeen =
                    state.seenKeys.has(key) ||
                    (asset.id != null && state.assetIdSet?.has?.(assetId));
                if (!alreadySeen) {
                    const sortKey = readGridQueryFromDataset(gridContainer?.dataset || {}).sort || "mtime_desc";
                    const assetToInsert = incomingIsPlaceholder
                        ? candidateAsset
                        : _clearLivePlaceholderState({ ...candidateAsset });
                    const insertPos = findInsertPosition(state.assets, assetToInsert, sortKey);
                    state.seenKeys.add(key);
                    if (asset.id != null) state.assetIdSet?.add?.(assetId);
                    state.assets.splice(insertPos, 0, assetToInsert); // findInsertPosition never returns -1
                    modified = true;
                }
            }
        }
        if (modified && vg) {
            dedupeAssetsByKey(state, deps);
            try {
                gridContainer.dataset.mjrHiddenPngSiblings = String(
                    Number(state.hiddenPngSiblings || 0) || 0,
                );
            } catch (e) {
                console.debug?.(e);
            }
            vg.setItems(state.assets);
        }
    } finally {
        batchState.flushing = false;
        // Items may have arrived during the flush — reschedule if needed (BUG-01).
        // Use a short delay (1 frame) instead of the full debounce so rapid
        // generation events chain quickly instead of waiting 200 ms between
        // each visible card insertion.
        if (batchState.pending.size > 0 && !batchState.timer) {
            const followUpDelay = Math.min(16, deps.debounceMs);
            batchState.timer = setTimeout(() => {
                batchState.timer = null;
                flushUpsertBatch(gridContainer, deps);
            }, followUpDelay);
        }
    }
}

export function upsertAsset(gridContainer: any, asset: any, deps: any) {
    if (!asset || !asset.id) return false;
    const state = deps.getOrCreateState(gridContainer);
    const assetId = String(asset.id);
    const vg = deps.ensureVirtualGrid(gridContainer, state);
    if (!vg) return false;
    const batchState = getUpsertBatchState(gridContainer, deps.upsertState);
    batchState.pending.set(assetId, asset);
    if (batchState.pending.size >= deps.maxBatchSize) {
        flushUpsertBatch(gridContainer, deps);
    } else if (!batchState.timer && !batchState.flushing) {
        batchState.timer = setTimeout(() => {
            batchState.timer = null;
            flushUpsertBatch(gridContainer, deps);
        }, deps.debounceMs);
    }
    return true;
}
