/**
 * GridContextMenu.js
 *
 * Vue now renders the menu DOM. This module only:
 * - detects right-click context
 * - computes the menu model
 * - executes menu actions
 */

import {
    browserFolderOp,
    collectFiles,
    deleteAsset,
    deleteWorkflow,
    duplicateWorkflow,
    getAssetMetadata,
    getWorkflowContent,
    listWorkflowThumbnailCandidates,
    markWorkflowLoaded,
    moveWorkflow,
    openInFolder,
    post,
    removeFilepathsFromCollection,
    renameAsset,
    setWorkflowThumbnail,
    updateAssetRating,
} from "../../api/client.js";
import { ENDPOINTS, buildBatchZipDownloadURL, buildDownloadURL } from "../../api/endpoints.js";
import { ASSET_RATING_CHANGED_EVENT } from "../../app/events.js";
import { t } from "../../app/i18n.js";
import { comfyConfirm, comfyPrompt } from "../../app/dialogs.js";
import { comfyToast } from "../../app/toast.js";
import { APP_CONFIG } from "../../app/config.js";
import { createWorkflowDot } from "../../components/Badges.js";
import { normalizeRenameFilename, validateFilename } from "../../utils/filenames.js";
import { reportError } from "../../utils/logging.js";
import { safeClosest, safeEscapeSelector } from "../../utils/dom.js";
import { safeDispatchCustomEvent } from "../../utils/events.js";
import { confirmDeletion } from "../../utils/deleteGuard.js";
import { showAddToCollectionMenu } from "../collections/contextmenu/addToCollectionMenu.js";
import { removeAssetsFromGrid } from "../grid/gridApi.js";
import {
    getRawHostApp,
    importWorkflowPreferHostTab,
    isHostWorkflowDirty,
} from "../../app/hostAdapter.js";
import { createSecureToken, pickRootId } from "../../utils/ids.js";
import { getShortcutDisplay } from "../grid/GridKeyboard.js";
import {
    closeAllGridContextMenus,
    openGridContextMenu,
    openGridTagsPopover,
} from "./gridContextMenuState.js";
import { cancelAllRatingUpdates, scheduleRatingUpdate } from "./ratingUpdater.js";
import { createCanvasLoaderNodes } from "../dnd/canvasLoaderNode.js";
import { stageToInputDetailed } from "../dnd/staging/stageToInput.js";
import { resolveAssetWorkflow } from "../viewer/workflowGraphMap/workflowGraphMapData.js";
import { openWorkflowAssetPicker, openWorkflowPicker } from "../workflows/workflowPickerState.js";
import { openWorkflowInfoDialog } from "../workflows/workflowInfoState.js";

let _nextMenuItemId = 1;
let viewerOpenRequestModulePromise: Promise<typeof import("../viewer/viewerOpenRequest.js")> | null = null;
let floatingViewerManagerModulePromise: Promise<typeof import("../viewer/floatingViewerManager.js")> | null = null;

function loadViewerOpenRequestModule() {
    if (!viewerOpenRequestModulePromise) {
        viewerOpenRequestModulePromise = import("../viewer/viewerOpenRequest.js");
    }
    return viewerOpenRequestModulePromise;
}

function loadFloatingViewerManagerModule() {
    if (!floatingViewerManagerModulePromise) {
        floatingViewerManagerModulePromise = import("../viewer/floatingViewerManager.js");
    }
    return floatingViewerManagerModulePromise;
}

function inferItemTone(label: any) {
    const normalized = String(label || "").trim().toLowerCase();
    if (!normalized) return "";
    if (normalized.includes("open graph map")) return "graph-map";
    if (normalized.includes("open in floating viewer") || normalized.includes("open floating viewer")) {
        return "floating-viewer";
    }
    if (normalized.includes("rename")) return "rename";
    if (normalized.includes("delete")) return "delete";
    return "";
}

function createItem(
    label: any,
    iconClass: any,
    rightHint: any,
    action: any,
    { disabled = false, closeOnSelect = true, submenu = null }: { disabled?: boolean; closeOnSelect?: boolean; submenu?: any[] | null } = {},
) {
    return {
        id: `mjr-grid-menu-item-${_nextMenuItemId++}`,
        type: "item",
        label: String(label || ""),
        iconClass: iconClass ? String(iconClass) : "",
        rightHint: rightHint ? String(rightHint) : "",
        tone: inferItemTone(label),
        disabled: !!disabled,
        closeOnSelect,
        submenu: Array.isArray(submenu) && submenu.length ? submenu : null,
        action: typeof action === "function" ? action : null,
    };
}

function createSeparator(): any {
    return {
        id: `mjr-grid-menu-separator-${_nextMenuItemId++}`,
        type: "separator",
    };
}

const getSelectedAssetIds = (gridContainer: any) => {
    const selected = new Set();
    if (!gridContainer) return selected;
    try {
        const raw = gridContainer.dataset?.mjrSelectedAssetIds;
        if (raw) {
            const list = JSON.parse(raw);
            if (Array.isArray(list)) {
                list.forEach((id: any) => selected.add(String(id)));
                return selected;
            }
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    try {
        const cards = gridContainer.querySelectorAll(".mjr-asset-card.is-selected");
        for (const card of cards) {
            const id = card.dataset?.mjrAssetId;
            if (id) selected.add(String(id));
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    return selected;
};

const getSelectedAssets = (gridContainer: any) => {
    const assets: any[] = [];
    if (!gridContainer) return assets;
    try {
        const selected =
            typeof gridContainer?._mjrGetSelectedAssets === "function"
                ? gridContainer._mjrGetSelectedAssets()
                : [];
        if (Array.isArray(selected) && selected.length) {
            return selected.filter((asset: any) => asset && typeof asset === "object");
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    try {
        const raw = gridContainer.dataset?.mjrSelectedAssetIds;
        if (raw) {
            const ids = JSON.parse(raw);
            if (Array.isArray(ids) && ids.length) {
                const all =
                    typeof gridContainer?._mjrGetAssets === "function"
                        ? gridContainer._mjrGetAssets()
                        : [];
                const idSet = new Set(ids.map(String));
                for (const asset of all || []) {
                    const id = String(asset?.id || "");
                    if (id && idSet.has(id)) assets.push(asset);
                }
                if (assets.length) return assets;
            }
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    try {
        const cards = gridContainer.querySelectorAll(".mjr-asset-card.is-selected");
        for (const card of cards) {
            const asset = card?._mjrAsset;
            if (asset) assets.push(asset);
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    return assets;
};

async function enrichWorkflowGraphAsset(asset: any) {
    const filepath = String(asset?.filepath || asset?.path || asset?.full_path || "").trim();
    if (!filepath) return asset;
    try {
        const result = await getWorkflowContent(filepath, { timeoutMs: 25_000 });
        const workflow = result?.data?.workflow || result?.workflow || null;
        const prompt = result?.data?.prompt || result?.prompt || null;
        if (!workflow && !prompt) return asset;
        return {
            ...asset,
            workflow: workflow || asset?.workflow,
            prompt: prompt || asset?.prompt,
            metadata_raw: {
                ...(asset?.metadata_raw && typeof asset.metadata_raw === "object"
                    ? asset.metadata_raw
                    : {}),
                ...(workflow ? { workflow } : {}),
                ...(prompt ? { prompt } : {}),
            },
        };
    } catch (e: any) {
        console.debug?.(e);
        return asset;
    }
}

const getAssetFilepath = (asset: any) => {
    try {
        const filepath = asset?.filepath || asset?.path || asset?.file_info?.filepath || "";
        return filepath ? String(filepath) : "";
    } catch {
        return "";
    }
};

const buildDndPayloadFromAsset = (asset: any) => {
    if (!asset || typeof asset !== "object") return null;
    const filename = String(asset.filename || "").trim();
    if (!filename) return null;
    return {
        filename,
        subfolder: asset.subfolder || "",
        type: String(asset.type || asset.source || "output").toLowerCase(),
        root_id: pickRootId(asset) || undefined,
        kind: String(asset.kind || "").toLowerCase(),
    };
};

const buildBatchZipItemFromAsset = (asset: any) => {
    if (!asset || typeof asset !== "object") return null;
    const filename = String(asset.filename || asset.name || "").trim();
    if (!filename) return null;
    return {
        filename,
        subfolder: asset.subfolder || "",
        type: String(asset.type || asset.source || "output").toLowerCase(),
        root_id: pickRootId(asset) || undefined,
    };
};

function triggerDownload(url: any, filename = "") {
    if (!url) return;
    const link = document.createElement("a");
    link.href = url;
    if (filename) link.download = filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

async function downloadSelectedAsZip(assets: any) {
    const list = Array.isArray(assets) ? assets.filter((entry: any) => !_isFolderAsset(entry)) : [];
    const items = list.map(buildBatchZipItemFromAsset).filter(Boolean);
    if (items.length < 2) {
        comfyToast("Select at least two files to download a ZIP.", "warning", 2600);
        return;
    }

    let token = "";
    try {
        token = createSecureToken("mjr_", 24);
    } catch (error: any) {
        reportError(error, "[GridContextMenu] Batch ZIP token", {
            showToast: APP_CONFIG.DEBUG_VERBOSE_ERRORS,
        });
        comfyToast("Could not create a secure ZIP token.", "error");
        return;
    }

    comfyToast(`Preparing ZIP for ${items.length} files...`, "info", 1800);
    const res = await post(ENDPOINTS.BATCH_ZIP_CREATE, {
        token,
        items,
        strip_metadata: false,
    });
    if (!res?.ok) {
        comfyToast(res?.error || "Failed to create ZIP.", "error");
        return;
    }

    const filename = res?.data?.filename || `Majoor_Batch_${items.length}.zip`;
    triggerDownload(buildBatchZipDownloadURL(token), filename);
    comfyToast(`Downloading ${filename}...`, "info", 3000);
}

async function loadAssetsToCanvas(assets: any) {
    const list = Array.isArray(assets) ? assets.filter(Boolean) : [];
    const stagedItems = await Promise.all(
        list.map(async (asset) => {
            const payload = buildDndPayloadFromAsset(asset);
            if (!payload) return null;
            const staged = await stageToInputDetailed({
                post,
                endpoint: ENDPOINTS.STAGE_TO_INPUT,
                payload,
                index: false,
            });
            const relativePath = staged?.relativePath;
            if (!relativePath) return null;
            return {
                payload,
                relativePath,
                droppedExt:
                    String(payload.filename || "")
                        .split(".")
                        .pop() || "",
            };
        }),
    );
    return createCanvasLoaderNodes({
        app: getRawHostApp(),
        items: stagedItems.filter(Boolean),
        event: null,
    });
}

const clearSelection = (gridContainer: any) => {
    if (!gridContainer) return;
    try {
        if (typeof gridContainer?._mjrSetSelection === "function") {
            gridContainer._mjrSetSelection([], "");
        } else {
            const cards = gridContainer.querySelectorAll(".mjr-asset-card.is-selected");
            for (const card of cards) {
                card.classList.remove("is-selected");
                card.setAttribute("aria-selected", "false");
            }
            delete gridContainer.dataset.mjrSelectedAssetIds;
            delete gridContainer.dataset.mjrSelectedAssetId;
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    safeDispatchCustomEvent(
        "mjr:selection-changed",
        { selectedIds: [] },
        { target: gridContainer, warnPrefix: "[GridContextMenu]" },
    );
};

const selectCardForDetails = (gridContainer: any, card: any, asset: any, state: any) => {
    if (!gridContainer) return;
    try {
        clearSelection(gridContainer);
    } catch (e: any) {
        console.debug?.(e);
    }

    try {
        card?.classList?.add?.("is-selected");
        card?.setAttribute?.("aria-selected", "true");
    } catch (e: any) {
        console.debug?.(e);
    }

    const id = asset?.id != null ? String(asset.id) : "";
    if (id) {
        try {
            if (typeof gridContainer?._mjrSetSelection === "function") {
                gridContainer._mjrSetSelection([id], id);
            } else {
                gridContainer.dataset.mjrSelectedAssetIds = JSON.stringify([id]);
                gridContainer.dataset.mjrSelectedAssetId = id;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
    }

    if (state && typeof state === "object") {
        try {
            state.selectedAssetIds = id ? [id] : [];
            state.activeAssetId = id;
        } catch (e: any) {
            console.debug?.(e);
        }
    }
};

const getAllAssetsInGrid = (gridContainer: any) => {
    const assets: any[] = [];
    if (!gridContainer) return assets;
    try {
        const getter = gridContainer._mjrGetAssets;
        if (typeof getter === "function") {
            const list = getter();
            if (Array.isArray(list) && list.length) return list;
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    try {
        const cards = gridContainer.querySelectorAll(".mjr-asset-card");
        for (const card of cards) {
            const asset = card?._mjrAsset;
            if (asset) assets.push(asset);
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    return assets;
};

const findIndexById = (assets: any, assetId: any) => {
    try {
        const id = String(assetId ?? "");
        return (assets || []).findIndex((asset: any) => String(asset?.id ?? "") === id);
    } catch {
        return -1;
    }
};

function findRenderedCard(assetId: any) {
    if (assetId == null) return null;
    try {
        return document.querySelector(`[data-mjr-asset-id="${safeEscapeSelector(assetId)}"]`);
    } catch (e: any) {
        console.debug?.(e);
        return null;
    }
}

function syncRenderedCardAsset(asset: any, patch: any = null) {
    const card = findRenderedCard(asset?.id);
    if (!card || !(card as any)._mjrAsset || typeof (card as any)._mjrAsset !== "object") return card;
    try {
        if (patch && typeof patch === "object") Object.assign((card as any)._mjrAsset, patch);
        else if (asset && typeof asset === "object") Object.assign((card as any)._mjrAsset, asset);
    } catch (e: any) {
        console.debug?.(e);
    }
    return card;
}

const triggerBrowserGridReload = (gridContainer: any) => {
    try {
        const reason = gridContainer ? "grid-contextmenu" : "grid-contextmenu-global";
        window?.dispatchEvent?.(new CustomEvent("mjr:reload-grid", { detail: { reason } }));
    } catch (e: any) {
        console.debug?.(e);
    }
};

async function resetIndexForSingleAsset(asset: any, gridContainer: any) {
    if (!asset) return { ok: false, error: "Missing asset" };

    const fileEntry = {
        filename: String(asset.filename || "").trim(),
        subfolder: String(asset.subfolder || "").trim(),
        type:
            String(asset.type || asset.source || "output")
                .trim()
                .toLowerCase() || "output",
        root_id:
            String(asset.root_id || asset.rootId || asset?.file_info?.root_id || "").trim() ||
            undefined,
    };
    if (!fileEntry.filename) return { ok: false, error: "Missing filename" };

    const indexRes = await post(ENDPOINTS.INDEX_FILES, { files: [fileEntry], incremental: false });
    if (!indexRes?.ok) return indexRes || { ok: false, error: "Index refresh failed" };

    if (asset?.id != null) {
        try {
            const fresh = await getAssetMetadata(asset.id);
            if (fresh?.ok && fresh?.data && typeof fresh.data === "object") {
                Object.assign(asset, fresh.data);
                const card = syncRenderedCardAsset(asset, fresh.data);
                const oldDot = card?.querySelector?.(".mjr-workflow-dot");
                if (oldDot) {
                    const nextDot = createWorkflowDot((card as any)._mjrAsset);
                    if (nextDot) oldDot.replaceWith(nextDot);
                }
            }
        } catch (e: any) {
            console.debug?.(e);
        }
    }

    triggerBrowserGridReload(gridContainer);
    return { ok: true, data: { refreshed: true } };
}

function setRating(asset: any, rating: any, onChanged: any) {
    const assetId = asset?.id;
    try {
        asset.rating = rating;
    } catch (e: any) {
        console.debug?.(e);
    }
    try {
        onChanged?.();
    } catch (e: any) {
        console.debug?.(e);
    }

    if (assetId) {
        scheduleRatingUpdate(String(assetId), rating, {
            successMessage: rating > 0 ? `Rating set to ${rating} stars` : "Rating cleared",
            errorMessage: "Failed to update rating",
            warnPrefix: "[GridContextMenu]",
            onSuccess: () => {
                safeDispatchCustomEvent(
                    ASSET_RATING_CHANGED_EVENT,
                    { assetId: String(assetId), rating },
                    { warnPrefix: "[GridContextMenu]" },
                );
            },
            onFailure: (error) => {
                reportError(error, "[GridContextMenu] Rating update", {
                    showToast: APP_CONFIG.DEBUG_VERBOSE_ERRORS,
                });
            },
        });
        return;
    }

    updateAssetRating(asset, rating).catch((error: any) => {
        reportError(error, "[GridContextMenu] Rating update", {
            showToast: APP_CONFIG.DEBUG_VERBOSE_ERRORS,
        });
    });
}

function _isBrowserScope(panelState: any) {
    return String(panelState?.scope || "").toLowerCase() === "custom";
}

function _isWorkflowScope(panelState: any) {
    return String(panelState?.scope || "").toLowerCase() === "workflow";
}

function _isFolderAsset(asset: any) {
    return String(asset?.kind || "").toLowerCase() === "folder";
}

function _isWorkflowAsset(asset: any) {
    return String(asset?.kind || "").toLowerCase() === "workflow";
}

function _isWorkflowThumbnailSourceAsset(asset: any) {
    if (!asset || _isFolderAsset(asset) || _isWorkflowAsset(asset)) return false;
    const filepath = getAssetFilepath(asset);
    if (!filepath) return false;
    const kind = String(asset?.kind || "").toLowerCase();
    if (kind === "image" || kind === "video") return true;
    const ext = String(filepath.split(".").pop() || "").toLowerCase();
    return ["png", "jpg", "jpeg", "webp", "gif", "mp4", "webm", "mov", "mkv", "avi", "m4v"].includes(ext);
}

async function _resolveHydratedAssetWorkflow(asset: any) {
    if (!asset || _isFolderAsset(asset)) return null;
    const directWorkflow = resolveAssetWorkflow(asset);
    if (directWorkflow) return directWorkflow;

    const assetId = asset?.id;
    if (assetId == null || assetId === "") return null;

    try {
        const result = await getAssetMetadata(assetId, { timeoutMs: 30_000 });
        const freshAsset = result?.data;
        if (result?.ok && freshAsset && typeof freshAsset === "object") {
            Object.assign(asset, freshAsset);
            return resolveAssetWorkflow(asset);
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    return null;
}

async function _openAssetWorkflowAsNewTab(asset: any) {
    const workflow = await _resolveHydratedAssetWorkflow(asset);
    if (!workflow || typeof workflow !== "object") {
        comfyToast(
            t(
                "toast.assetWorkflowMissing",
                "No embedded ComfyUI workflow was found for this asset.",
            ),
            "warn",
            2600,
        );
        return;
    }

    const hostApp = getRawHostApp();
    const importResult = importWorkflowPreferHostTab(workflow, hostApp);
    if (!importResult.ok) {
        comfyToast(
            t(
                "toast.workflowImportUnavailable",
                "ComfyUI workflow import is unavailable in this frontend.",
            ),
            "error",
        );
        return;
    }

    const message =
        importResult.mode === "new-tab"
            ? t("toast.workflowLoadedNewTab", "Workflow loaded in a new ComfyUI tab.")
            : t("toast.workflowLoaded", "Workflow loaded");
    comfyToast(message, "success", 1800);
}

async function _loadWorkflowAsset(asset: any) {
    const filepath = String(asset?.filepath || asset?.path || asset?.full_path || "").trim();
    if (!filepath) {
        comfyToast(t("toast.workflowMissingPath", "Workflow file path is missing."), "error");
        return;
    }
    const hostApp = getRawHostApp();
    const dirtyState = isHostWorkflowDirty(hostApp);
    if (dirtyState === true) {
        const ok = await comfyConfirm(
            t(
                "dialog.workflowLoadReplaceDirty",
                "Current canvas has unsaved changes. Replace it with this workflow?",
            ),
            t("tab.workflow", "Workflow"),
        );
        if (!ok) return;
    }
    const result = await getWorkflowContent(filepath, { timeoutMs: 30_000 });
    const workflow = result?.data?.workflow || result?.workflow || null;
    if (!result?.ok || !workflow || typeof workflow !== "object") {
        comfyToast(result?.error || t("toast.workflowLoadFailed", "Failed to load workflow."), "error");
        return;
    }
    const importResult = importWorkflowPreferHostTab(workflow, hostApp);
    if (!importResult.ok) {
        // Host API unavailable: fallback to opening/downloading JSON so user can still import manually.
        const downloadUrl = buildDownloadURL(filepath, { inline: true });
        try {
            if (downloadUrl && typeof window !== "undefined") {
                window.open(downloadUrl, "_blank", "noopener,noreferrer");
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        comfyToast(
            t(
                "toast.workflowImportFallback",
                "ComfyUI workflow import is unavailable in this frontend. Opened workflow JSON for manual import.",
            ),
            "warn",
        );
        if (importResult.mode === "new-tab") {
            comfyToast(t("toast.workflowLoadedNewTab", "Workflow loaded in a new ComfyUI tab."), "success");
        }
        return;
    }
    try {
        await markWorkflowLoaded({ filepath }, { timeoutMs: 10_000 });
    } catch (e: any) {
        console.debug?.(e);
    }
    comfyToast(t("toast.workflowLoaded", "Workflow loaded"), "success", 1800);
}

function _requestWorkflowGridReload(reason: string) {
    try {
        window.dispatchEvent(new CustomEvent("mjr:reload-grid", { detail: { reason } }));
    } catch (e: any) {
        console.debug?.(e);
    }
}

async function _duplicateWorkflowAsset(asset: any) {
    const filepath = String(asset?.filepath || "").trim();
    const result = await duplicateWorkflow({ filepath }, { timeoutMs: 30_000 });
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.workflowSaveFailed", "Failed to save workflow."), "error");
        return;
    }
    comfyToast(t("toast.workflowSaved", "Workflow saved"), "success", 1800);
    _requestWorkflowGridReload("workflow-duplicate");
}

async function _renameWorkflowAsset(asset: any) {
    const filepath = String(asset?.filepath || "").trim();
    const current = String(asset?.display_name || asset?.filename || "").replace(/\.json$/i, "");
    const name = await comfyPrompt(t("ctx.renameWorkflow", "Rename workflow"), current, t("tab.workflow", "Workflow"));
    const safeName = String(name || "").trim();
    if (!safeName) return;
    const result = await moveWorkflow({ filepath, name: safeName }, { timeoutMs: 30_000 });
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.workflowSaveFailed", "Failed to save workflow."), "error");
        return;
    }
    comfyToast(t("toast.workflowUpdated", "Workflow updated"), "success", 1800);
    _requestWorkflowGridReload("workflow-rename");
}

async function _categorizeWorkflowAsset(asset: any) {
    const filepath = String(asset?.filepath || "").trim();
    const current = String(asset?.subfolder || "").trim();
    const category = await comfyPrompt(
        t("dialog.workflowCategory", "Workflow category"),
        current,
        t("tab.workflow", "Workflow"),
    );
    if (category === null || category === undefined) return;
    const result = await moveWorkflow({ filepath, category: String(category || "") }, { timeoutMs: 30_000 });
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.workflowSaveFailed", "Failed to save workflow."), "error");
        return;
    }
    comfyToast(t("toast.workflowUpdated", "Workflow updated"), "success", 1800);
    _requestWorkflowGridReload("workflow-categorize");
}

async function _setWorkflowThumbnailFromLinkedAsset(asset: any) {
    const filepath = String(asset?.filepath || "").trim();
    if (!filepath) {
        comfyToast(t("toast.noFilePath"), "error");
        return;
    }

    const candidatesRes = await listWorkflowThumbnailCandidates({ filepath, limit: 12 }, { timeoutMs: 15_000 });
    if (!candidatesRes?.ok) {
        comfyToast(candidatesRes?.error || t("toast.workflowLoadFailed", "Failed to load workflow."), "error");
        return;
    }

    const candidates = Array.isArray(candidatesRes.data) ? candidatesRes.data : [];
    if (!candidates.length) {
        comfyToast(
            t(
                "toast.workflowThumbnailNoCandidates",
                "No linked outputs are available for this workflow yet.",
            ),
            "warning",
            2600,
        );
        return;
    }

    const selectedCandidate = await openWorkflowAssetPicker({
        title: t("ctx.setWorkflowThumbnail", "Set workflow thumbnail"),
        workflow: asset,
        items: candidates,
    });
    if (!selectedCandidate?.filepath) return;

    const result = await setWorkflowThumbnail(
        { filepath, source_filepath: selectedCandidate.filepath },
        { timeoutMs: 30_000 },
    );
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.workflowSaveFailed", "Failed to save workflow."), "error");
        return;
    }

    comfyToast(t("toast.workflowUpdated", "Workflow updated"), "success", 1800);
    _requestWorkflowGridReload("workflow-thumbnail");
}

async function _exportWorkflowAsset(asset: any) {
    const filepath = String(asset?.filepath || "").trim();
    if (!filepath) {
        comfyToast(t("toast.workflowMissingPath", "Workflow file path is missing."), "error");
        return;
    }
    const result = await getWorkflowContent(filepath, { timeoutMs: 30_000 });
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.workflowLoadFailed", "Failed to load workflow."), "error");
        return;
    }
    const workflow = result?.data?.workflow || result?.workflow || null;
    if (!workflow || typeof workflow !== "object") {
        comfyToast(t("toast.workflowLoadFailed", "Failed to load workflow."), "error");
        return;
    }
    const filename = String(asset?.filename || asset?.display_name || "workflow.json")
        .replace(/[\\/:*?"<>|]+/g, "_")
        .replace(/\.json$/i, "") || "workflow";
    const blob = new Blob([`${JSON.stringify(workflow, null, 2)}\n`], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    try {
        const link = document.createElement("a");
        link.href = url;
        link.download = `${filename}.json`;
        link.rel = "noopener";
        document.body.appendChild(link);
        link.click();
        link.remove();
        comfyToast(t("toast.workflowExported", "Workflow exported"), "success", 1800);
    } finally {
        setTimeout(() => URL.revokeObjectURL(url), 5000);
    }
}

async function _showWorkflowInExplorer(asset: any) {
    const filepath = String(asset?.filepath || "").trim();
    if (!filepath) {
        comfyToast(t("toast.workflowMissingPath", "Workflow file path is missing."), "error");
        return;
    }
    const result = await openInFolder({ filepath });
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.openFolderFailed", "Failed to open folder."), "error");
        return;
    }
    comfyToast(t("toast.openedInFolder", "Opened in folder"), "success", 1600);
}


async function _assignAssetAsWorkflowThumbnail(asset: any) {
    const sourceFilepath = getAssetFilepath(asset);
    if (!sourceFilepath) {
        comfyToast(t("toast.noFilePath"), "error");
        return;
    }
    const selectedWorkflow = await openWorkflowPicker({
        title: t("ctx.assignAsWorkflowThumbnail", "Use as workflow thumbnail"),
        sourceAsset: asset,
    });
    const selectedWorkflowPath = String(selectedWorkflow?.filepath || "").trim();
    if (!selectedWorkflowPath) return;

    const sourceKind = String(asset?.kind || "").toLowerCase();
    const isVideo = sourceKind === "video" || /\.(mp4|webm|mov|mkv|avi|m4v)$/i.test(sourceFilepath);
    comfyToast(
        isVideo
            ? t("toast.workflowThumbnailConverting", "Preparing a 5 second animated workflow thumbnail...")
            : t("toast.workflowThumbnailApplying", "Applying workflow thumbnail..."),
        "info",
        2200,
    );
    const result = await setWorkflowThumbnail(
        { filepath: selectedWorkflowPath, source_filepath: sourceFilepath },
        { timeoutMs: isVideo ? 75_000 : 30_000 },
    );
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.workflowSaveFailed", "Failed to save workflow."), "error");
        return;
    }
    comfyToast(t("toast.workflowUpdated", "Workflow updated"), "success", 1800);
    _requestWorkflowGridReload("workflow-thumbnail-assign");
}

async function _deleteWorkflowAsset(asset: any) {
    const filepath = String(asset?.filepath || "").trim();
    const ok = await comfyConfirm(
        t("dialog.deleteWorkflowConfirm", "Delete this workflow JSON and its adjacent thumbnail files?"),
        t("ctx.deleteWorkflow", "Delete workflow"),
    );
    if (!ok) return;
    const result = await deleteWorkflow({ filepath }, { timeoutMs: 30_000 });
    if (!result?.ok) {
        comfyToast(result?.error || t("toast.fileDeleteFailed", "Failed to delete file."), "error");
        return;
    }
    comfyToast(t("toast.workflowDeleted", "Workflow deleted"), "success", 1800);
    _requestWorkflowGridReload("workflow-delete");
}

function _getSelectionSnapshot(gridContainer: any) {
    const selectedIds = getSelectedAssetIds(gridContainer);
    const selectedAssetsNow = getSelectedAssets(gridContainer);
    const effectiveSelectionCount = selectedAssetsNow.length || selectedIds.size;
    return {
        selectedIds,
        selectedAssetsNow,
        effectiveSelectionCount,
        isMultiSelected: effectiveSelectionCount > 1,
        hasSelection: effectiveSelectionCount > 0,
    };
}

function _ensureRightClickSelection({ asset, card, gridContainer }: { asset?: any; card?: any; gridContainer?: any } = {}) {
    let selectedIds = getSelectedAssetIds(gridContainer);
    const cardSelected = !!card?.classList?.contains?.("is-selected");
    if (!asset || cardSelected) return selectedIds;

    const nextId = asset?.id != null ? String(asset.id) : "";
    const newSelection = nextId ? [nextId] : [];
    try {
        if (nextId && typeof gridContainer?._mjrSetSelection === "function") {
            gridContainer._mjrSetSelection(newSelection, newSelection[0]);
        } else if (nextId) {
            gridContainer.dataset.mjrSelectedAssetIds = JSON.stringify(newSelection);
            gridContainer.dataset.mjrSelectedAssetId = newSelection[0];
        } else {
            delete gridContainer.dataset.mjrSelectedAssetIds;
            delete gridContainer.dataset.mjrSelectedAssetId;
        }
    } catch (e: any) {
        console.debug?.(e);
    }

    safeDispatchCustomEvent(
        "mjr:selection-changed",
        {
            selectedIds: newSelection,
            selectedAssets: [asset],
            activeId: nextId,
        },
        { target: gridContainer, warnPrefix: "[GridContextMenu]" },
    );

    selectedIds = new Set(newSelection);
    try {
        gridContainer.querySelectorAll(".mjr-asset-card.is-selected").forEach((el: any) => {
            el.classList.remove("is-selected");
            el.setAttribute?.("aria-selected", "false");
        });
        card?.classList?.add?.("is-selected");
        card?.setAttribute?.("aria-selected", "true");
    } catch (e: any) {
        console.debug?.(e);
    }
    return selectedIds;
}

function _buildRatingSubmenu(asset: any, canRate: any) {
    const stars = (n: any) => "*".repeat(n) + "o".repeat(Math.max(0, 5 - n));
    const updateRenderedRating = (value: any) => {
        syncRenderedCardAsset(asset, { rating: value });
    };

    return [
        createItem(
            stars(5),
            "pi pi-star",
            null,
            async () => {
                setRating(asset, 5, () => updateRenderedRating(5));
            },
            { disabled: !canRate },
        ),
        createItem(
            stars(4),
            "pi pi-star",
            null,
            async () => {
                setRating(asset, 4, () => updateRenderedRating(4));
            },
            { disabled: !canRate },
        ),
        createItem(
            stars(3),
            "pi pi-star",
            null,
            async () => {
                setRating(asset, 3, () => updateRenderedRating(3));
            },
            { disabled: !canRate },
        ),
        createItem(
            stars(2),
            "pi pi-star",
            null,
            async () => {
                setRating(asset, 2, () => updateRenderedRating(2));
            },
            { disabled: !canRate },
        ),
        createItem(
            stars(1),
            "pi pi-star",
            null,
            async () => {
                setRating(asset, 1, () => updateRenderedRating(1));
            },
            { disabled: !canRate },
        ),
        createSeparator(),
        createItem(
            t("ctx.resetRating", "Reset rating"),
            "pi pi-star",
            "0",
            async () => {
                setRating(asset, 0, () => updateRenderedRating(0));
            },
            { disabled: !canRate },
        ),
    ];
}

function _buildEmptyGridItems({ currentPath, gridContainer }: { currentPath?: any; gridContainer?: any } = {}) {
    return [
        createItem(
            t("ctx.createFolderHere", "Create folder here..."),
            "pi pi-plus",
            null,
            async () => {
                try {
                    const name = await comfyPrompt(
                        t("dialog.newFolderName", "New folder name"),
                        "",
                    );
                    const next = String(name || "").trim();
                    if (!next) return;
                    const res = await browserFolderOp({
                        op: "create",
                        path: currentPath,
                        name: next,
                    });
                    if (!res?.ok) {
                        comfyToast(res?.error || "Failed to create folder", "error");
                        return;
                    }
                    triggerBrowserGridReload(gridContainer);
                    comfyToast(
                        t("toast.folderCreated", "Folder created: {name}", { name: next }),
                        "success",
                    );
                } catch (error: any) {
                    comfyToast(
                        t("toast.createFolderFailedDetail", "Create folder failed: {error}", {
                            error: error?.message || String(error || ""),
                        }),
                        "error",
                    );
                }
            },
        ),
    ];
}

function _buildFolderItems({ asset, gridContainer, panelState }: { asset?: any; gridContainer?: any; panelState?: any } = {}) {
    const folderPath = String(asset?.filepath || "").trim();
    const items = [
        createItem(t("ctx.openFolder", "Open folder"), "pi pi-folder-open", null, () => {
            try {
                gridContainer.dispatchEvent(
                    new CustomEvent("mjr:open-folder-asset", {
                        bubbles: true,
                        detail: { asset },
                    }),
                );
            } catch (e: any) {
                console.debug?.(e);
            }
        }),
        createItem(t("ctx.pinAsBrowserRoot"), "pi pi-bookmark", null, async () => {
            if (!folderPath) {
                comfyToast(t("toast.unableResolveFolderPath"), "error");
                return;
            }
            const label = await comfyPrompt(
                t("dialog.browserRootLabelOptional"),
                String(asset?.filename || ""),
            );
            const res = await post(ENDPOINTS.CUSTOM_ROOTS, {
                path: folderPath,
                label: String(label || "").trim() || undefined,
            });
            if (!res?.ok) {
                comfyToast(res?.error || t("toast.pinFolderFailed"), "error");
                return;
            }
            comfyToast(t("toast.folderPinnedAsBrowserRoot"), "success");
            try {
                window.dispatchEvent(
                    new CustomEvent("mjr:custom-roots-changed", {
                        detail: { preferredId: String(res?.data?.id || "") },
                    }),
                );
            } catch (e: any) {
                console.debug?.(e);
            }
        }),
    ];

    if (_isBrowserScope(panelState) && folderPath) {
        items.push(
            createSeparator(),
            createItem(t("ctx.createFolderHere"), "pi pi-plus", null, async () => {
                try {
                    const name = await comfyPrompt(t("dialog.newFolderName"), "");
                    const next = String(name || "").trim();
                    if (!next) return;
                    const res = await browserFolderOp({
                        op: "create",
                        path: folderPath,
                        name: next,
                    });
                    if (!res?.ok) {
                        comfyToast(res?.error || t("toast.createFolderFailed"), "error");
                        return;
                    }
                    triggerBrowserGridReload(gridContainer);
                    comfyToast(t("toast.folderCreated", { name: next }), "success");
                } catch (error: any) {
                    comfyToast(
                        t("toast.createFolderFailedDetail", {
                            error: error?.message || String(error || ""),
                        }),
                        "error",
                    );
                }
            }),
            createItem(t("ctx.renameFolder"), "pi pi-pencil", null, async () => {
                try {
                    const current = String(asset?.filename || "").trim();
                    const name = await comfyPrompt(t("dialog.renameFolder"), current);
                    const next = String(name || "").trim();
                    if (!next || next === current) return;
                    const res = await browserFolderOp({
                        op: "rename",
                        path: folderPath,
                        name: next,
                    });
                    if (!res?.ok) {
                        comfyToast(res?.error || t("toast.renameFolderFailed"), "error");
                        return;
                    }
                    triggerBrowserGridReload(gridContainer);
                    comfyToast(t("toast.folderRenamed"), "success");
                } catch (error: any) {
                    comfyToast(
                        t("toast.renameFolderFailedDetail", {
                            error: error?.message || String(error || ""),
                        }),
                        "error",
                    );
                }
            }),
            createItem(t("ctx.moveFolder"), "pi pi-arrow-right", null, async () => {
                try {
                    const destination = await comfyPrompt(t("dialog.destinationDirectoryPath"), "");
                    const dest = String(destination || "").trim();
                    if (!dest) return;
                    const res = await browserFolderOp({
                        op: "move",
                        path: folderPath,
                        destination: dest,
                    });
                    if (!res?.ok) {
                        comfyToast(res?.error || t("toast.moveFolderFailed"), "error");
                        return;
                    }
                    triggerBrowserGridReload(gridContainer);
                    comfyToast(t("toast.folderMoved"), "success");
                } catch (error: any) {
                    comfyToast(
                        t("toast.moveFolderFailedDetail", {
                            error: error?.message || String(error || ""),
                        }),
                        "error",
                    );
                }
            }),
            createItem(t("ctx.deleteFolder"), "pi pi-trash", null, async () => {
                try {
                    const folderName = String(
                        asset?.filename || t("label.thisFolder", "this folder"),
                    );
                    const ok = await comfyConfirm(
                        t("dialog.deleteFolderRecursive", { name: folderName }),
                    );
                    if (!ok) return;
                    const res = await browserFolderOp({
                        op: "delete",
                        path: folderPath,
                        recursive: true,
                    });
                    if (!res?.ok) {
                        comfyToast(res?.error || t("toast.deleteFolderFailed"), "error");
                        return;
                    }
                    triggerBrowserGridReload(gridContainer);
                    comfyToast(t("toast.folderDeleted"), "success");
                } catch (error: any) {
                    comfyToast(
                        t("toast.deleteFolderFailedDetail", {
                            error: error?.message || String(error || ""),
                        }),
                        "error",
                    );
                }
            }),
        );
    }

    return items;
}

function _buildAssetItems({
    asset,
    card,
    gridContainer,
    panelState,
    pointerX,
    pointerY,
    selection,
}: { asset?: any; card?: any; gridContainer?: any; panelState?: any; pointerX?: any; pointerY?: any; selection?: any } = {}) {
    const { selectedAssetsNow, effectiveSelectionCount, isMultiSelected, hasSelection } = selection;
    const items: any[] = [];
    const inWorkflowScope = _isWorkflowScope(panelState);
    const openInViewer = ({ assets = [] as any[], index = 0, mode = "" } = {}) =>
        loadViewerOpenRequestModule()
            .then((mod) => mod.requestViewerOpen({ assets, index, mode }))
            .catch((e) => console.debug?.(e));

    if (_isWorkflowAsset(asset)) {
        items.push(
            createItem(
                t("ctx.loadWorkflow", "Load workflow"),
                "pi pi-sitemap",
                null,
                () => _loadWorkflowAsset(asset),
            ),
            createItem(
                t("ctx.duplicateWorkflow", "Duplicate workflow"),
                "pi pi-copy",
                null,
                () => _duplicateWorkflowAsset(asset),
            ),
            createItem(
                t("ctx.renameWorkflow", "Rename workflow"),
                "pi pi-pencil",
                null,
                () => _renameWorkflowAsset(asset),
            ),
            createItem(
                t("ctx.categorizeWorkflow", "Set workflow category"),
                "pi pi-folder",
                null,
                () => _categorizeWorkflowAsset(asset),
            ),
            createItem(
                t("ctx.setWorkflowThumbnail", "Set workflow thumbnail"),
                "pi pi-image",
                null,
                () => _setWorkflowThumbnailFromLinkedAsset(asset),
            ),
            createItem(
                t("ctx.editWorkflowInfo", "Edit infos"),
                "pi pi-info-circle",
                null,
                () => openWorkflowInfoDialog(asset),
            ),
            createItem(
                t("ctx.exportWorkflow", "Export workflow"),
                "pi pi-download",
                null,
                () => _exportWorkflowAsset(asset),
            ),
            createItem(
                t("ctx.showInExplorer", "Show in Explorer"),
                "pi pi-folder-open",
                null,
                () => _showWorkflowInExplorer(asset),
            ),
            createItem(
                t("ctx.deleteWorkflow", "Delete workflow"),
                "pi pi-trash",
                null,
                () => _deleteWorkflowAsset(asset),
            ),
            createSeparator(),
        );

        if (inWorkflowScope) {
            items.push(
                createItem("Open Graph Map", "pi pi-sitemap", null, async () => {
                    try {
                        const graphAsset = await enrichWorkflowGraphAsset(asset);
                        const { floatingViewerManager } = await loadFloatingViewerManagerModule();
                        await floatingViewerManager.openAssets({
                            assets: [graphAsset],
                            index: 0,
                            mode: "graph",
                        });
                    } catch (e: any) {
                        console.debug?.(e);
                    }
                }),
            );
            return items;
        }
    } else if (inWorkflowScope) {
        return items;
    }

    items.push(
        createItem("Open As New Tab", "pi pi-external-link", null, () =>
            _openAssetWorkflowAsNewTab(asset),
        ),
        createItem("Open Viewer", "pi pi-image", getShortcutDisplay("OPEN_VIEWER"), () => {
            try {
                const selectedAssets = hasSelection ? getSelectedAssets(gridContainer) : [];
                const mediaSelectedAssets = selectedAssets.filter(
                    (entry) => !_isFolderAsset(entry),
                );
                const selectedCount = mediaSelectedAssets.length;
                if (selectedCount >= 2 && selectedCount <= 4) {
                    openInViewer({ assets: mediaSelectedAssets, index: 0 });
                    return;
                }

                const allAssets = getAllAssetsInGrid(gridContainer).filter(
                    (entry) => !_isFolderAsset(entry),
                );
                const index = findIndexById(allAssets, asset?.id);
                if (!allAssets.length || index < 0) return;
                openInViewer({ assets: allAssets, index: Math.max(0, index) });
            } catch (e: any) {
                console.debug?.(e);
            }
        }),
        createItem(
            "Open in Floating Viewer",
            "pi pi-window-maximize",
            getShortcutDisplay("OPEN_FLOATING_VIEWER"),
            async () => {
                try {
                    const { floatingViewerManager } = await loadFloatingViewerManagerModule();
                    const selectedAssets = hasSelection ? getSelectedAssets(gridContainer) : [];
                    const mediaSelectedAssets = selectedAssets.filter(
                        (entry) => !_isFolderAsset(entry),
                    );
                    if (mediaSelectedAssets.length) {
                        await floatingViewerManager.openAssets({
                            assets: mediaSelectedAssets,
                            index: Math.max(
                                0,
                                mediaSelectedAssets.findIndex(
                                    (entry) => String(entry?.id || "") === String(asset?.id || ""),
                                ),
                            ),
                        });
                        return;
                    }
                    await floatingViewerManager.openAssets({ assets: [asset], index: 0 });
                } catch (e: any) {
                    console.debug?.(e);
                }
            },
        ),
        createItem("Load Asset", "pi pi-upload", "L / L+Drop", async () => {
            const selectedAssets = hasSelection ? getSelectedAssets(gridContainer) : [];
            const selectedMediaAssets = selectedAssets.filter((entry: any) => !_isFolderAsset(entry));
            const list = selectedMediaAssets.length ? selectedMediaAssets : [asset];
            const created = await loadAssetsToCanvas(list);
            if (!created) {
                comfyToast("Failed to create a loader node for this asset.", "error");
            }
        }),
        createItem("Open Graph Map", "pi pi-sitemap", null, async () => {
            try {
                const graphAsset = await enrichWorkflowGraphAsset(asset);
                const { floatingViewerManager } = await loadFloatingViewerManagerModule();
                await floatingViewerManager.openAssets({
                    assets: [graphAsset],
                    index: 0,
                    mode: "graph",
                });
            } catch (e: any) {
                console.debug?.(e);
            }
        }),
        createItem(
            t("ctx.showMetadataPanel", "Show metadata panel"),
            "pi pi-info-circle",
            getShortcutDisplay("METADATA_PANEL"),
            () => {
                try {
                    selectCardForDetails(gridContainer, card, asset, panelState);
                    const openDetails = gridContainer?._mjrOpenDetails;
                    if (typeof openDetails === "function") openDetails();
                } catch (e: any) {
                    console.debug?.(e);
                }
            },
        ),
    );

    if (_isWorkflowThumbnailSourceAsset(asset)) {
        items.push(
            createItem(
                t("ctx.assignAsWorkflowThumbnail", "Use as workflow thumbnail"),
                "pi pi-image",
                null,
                () => _assignAssetAsWorkflowThumbnail(asset),
            ),
        );
    }

    const mediaSelectedAssets = (hasSelection ? selectedAssetsNow : [] as any[]).filter(
        (entry: any) => !_isFolderAsset(entry),
    );
    const selectedCount = mediaSelectedAssets.length;

    if (selectedCount === 2) {
        items.push(
            createItem(
                "Compare A/B (2 selected)",
                "pi pi-clone",
                getShortcutDisplay("COMPARE_AB"),
                () => {
                    try {
                        openInViewer({ assets: mediaSelectedAssets, index: 0, mode: "ab" });
                    } catch (e: any) {
                        console.debug?.(e);
                    }
                },
            ),
        );
    }

    if (selectedCount >= 2 && selectedCount <= 4) {
        items.push(
            createItem(
                `Side-by-side (2x2) (${selectedCount} selected)`,
                "pi pi-table",
                getShortcutDisplay("SIDE_BY_SIDE"),
                () => {
                    try {
                        openInViewer({
                            assets: mediaSelectedAssets,
                            index: 0,
                            mode: "sidebyside",
                        });
                    } catch (e: any) {
                        console.debug?.(e);
                    }
                },
            ),
        );
    }

    items.push(
        createItem(
            t("ctx.openInFolder", "Open in folder"),
            "pi pi-folder-open",
            getShortcutDisplay("OPEN_IN_FOLDER"),
            async () => {
                const res = await openInFolder(asset);
                if (!res?.ok) {
                    comfyToast(res?.error || t("toast.openFolderFailed"), "error");
                } else {
                    comfyToast(t("toast.openedInFolder"), "info", 2000);
                }
            },
            { disabled: !(asset?.id || getAssetFilepath(asset)) },
        ),
        createItem(
            t("ctx.collectFiles", "Collect files"),
            "pi pi-box",
            null,
            async () => {
                comfyToast(t("toast.collectingFiles", "Collecting files..."), "info", 2500);
                const res = await collectFiles(asset);
                if (!res?.ok) {
                    comfyToast(res?.error || t("toast.collectFilesFailed", "Collect files failed"), "error");
                    return;
                }
                const data = res?.data || {};
                const missingCount = Array.isArray(data.missing) ? data.missing.length : 0;
                const where = data.fallback_used
                    ? t("toast.collectFallbackDir", "output folder (source folder not writable)")
                    : t("toast.collectSameDir", "asset folder");
                let message = t("toast.collectedFiles", "Collected {name} in {where}", {
                    name: String(data.zip_name || "zip"),
                    where,
                });
                if (missingCount > 0) {
                    message += ` — ${missingCount} ${t("toast.collectMissingInputs", "input(s) missing")}`;
                }
                comfyToast(message, missingCount > 0 ? "warning" : "success", 6000);
            },
            { disabled: !getAssetFilepath(asset) },
        ),
        createItem(
            t("ctx.copyPath", "Copy path"),
            "pi pi-copy",
            getShortcutDisplay("COPY_PATH"),
            async () => {
                const filepath = asset?.filepath ? String(asset.filepath) : "";
                if (!filepath) {
                    comfyToast(t("toast.noFilePath"), "error");
                    return;
                }
                try {
                    await navigator.clipboard.writeText(filepath);
                    comfyToast(t("toast.pathCopied"), "success", 2000);
                } catch (err: any) {
                    console.warn(t("log.clipboardCopyFailed", "Clipboard copy failed"), err);
                    comfyToast(t("toast.pathCopyFailed"), "error");
                }
            },
        ),
        createItem(
            t("ctx.download", "Download"),
            "pi pi-download",
            getShortcutDisplay("DOWNLOAD"),
            () => {
                if (!asset?.filepath) return;
                const url = buildDownloadURL(asset.filepath);
                const filename = asset.filename || "download";
                triggerDownload(url, filename);
                comfyToast(
                    t("toast.downloadingFile", "Downloading {filename}...", { filename }),
                    "info",
                    3000,
                );
            },
            { disabled: !asset?.filepath },
        ),
        createItem(
            `Download selected as ZIP (${selectedCount})`,
            "pi pi-file-zip",
            null,
            async () => {
                await downloadSelectedAsZip(mediaSelectedAssets);
            },
            { disabled: selectedCount < 2 },
        ),
        createItem(
            "Add to collection...",
            "pi pi-bookmark",
            getShortcutDisplay("ADD_TO_COLLECTION"),
            async () => {
                const selectedAssets = getSelectedAssets(gridContainer);
                const list = selectedAssets.length ? selectedAssets : [asset];
                await showAddToCollectionMenu({ x: pointerX, y: pointerY, assets: list });
            },
        ),
    );

    const collectionId = String(panelState?.collectionId || "").trim();
    if (collectionId) {
        const selectedAssets = getSelectedAssets(gridContainer);
        const list = selectedAssets.length ? selectedAssets : [asset];
        const filepaths = list.map(getAssetFilepath).filter(Boolean);
        const label =
            filepaths.length > 1
                ? `Remove from collection (${filepaths.length})`
                : "Remove from collection";
        items.push(
            createItem(label, "pi pi-bookmark", null, async () => {
                if (!filepaths.length) {
                    comfyToast(t("toast.noFilePath"), "error");
                    return;
                }
                const res = await removeFilepathsFromCollection(collectionId, filepaths);
                if (!res?.ok) {
                    comfyToast(res?.error || t("toast.removeFromCollectionFailed"), "error");
                    return;
                }
                triggerBrowserGridReload(gridContainer);
            }),
        );
    }

    const canRate = !!(asset?.id || getAssetFilepath(asset));
    items.push(
        createSeparator(),
        createItem(
            t("ctx.editTags", "Edit tags"),
            "pi pi-tags",
            getShortcutDisplay("EDIT_TAGS"),
            () => {
                showTagsPopover(pointerX + 6, pointerY + 6, asset, () => {
                    syncRenderedCardAsset(asset, {
                        tags: Array.isArray(asset?.tags) ? [...asset.tags] : [],
                    });
                });
            },
            { closeOnSelect: false },
        ),
        createSeparator(),
        createItem(t("ctx.setRating", "Set rating"), "pi pi-star", "1-5 >", null, {
            disabled: !canRate,
            closeOnSelect: false,
            submenu: _buildRatingSubmenu(asset, canRate),
        }),
        createSeparator(),
        createItem(
            t("ctx.resetIndexFile", "Reset index (this file)"),
            "pi pi-refresh",
            null,
            async () => {
                if (!(asset?.filename || asset?.id)) return;
                comfyToast(t("toast.rescanningFile", "Rescanning file..."), "info", 1600);
                try {
                    const res = await resetIndexForSingleAsset(asset, gridContainer);
                    if (res?.ok) {
                        comfyToast(
                            t("toast.metadataRefreshed", "Metadata refreshed{suffix}", {
                                suffix: "",
                            }),
                            "success",
                            1800,
                        );
                    } else {
                        comfyToast(
                            res?.error || t("toast.resetFailed", "Failed to reset index"),
                            "error",
                        );
                    }
                } catch (error: any) {
                    comfyToast(
                        `${t("toast.resetFailed", "Failed to reset index")}: ${error?.message || String(error || "")}`,
                        "error",
                    );
                }
            },
            { disabled: !(asset?.filename || asset?.id) },
        ),
        createItem(
            "Rename...",
            "pi pi-pencil",
            getShortcutDisplay("RENAME"),
            async () => {
                if (!(asset?.id || getAssetFilepath(asset))) return;
                const currentName = asset.filename || "";
                const rawInput = await comfyPrompt(
                    t("dialog.rename.title", "Rename file"),
                    currentName,
                );
                const newName = normalizeRenameFilename(rawInput, currentName);
                if (!newName || newName === currentName) return;
                const validation = validateFilename(newName);
                if (!validation.valid) {
                    comfyToast(validation.reason, "error");
                    return;
                }

                try {
                    const result = await renameAsset(asset, newName);
                    if (!result?.ok) {
                        comfyToast(result?.error || t("toast.fileRenameFailed"), "error");
                        return;
                    }

                    const fresh = result?.data?.asset;
                    if (fresh && typeof fresh === "object") {
                        Object.assign(asset, fresh);
                        syncRenderedCardAsset(asset, fresh);
                    } else {
                        asset.filename = newName;
                        if (asset.filepath) {
                            asset.filepath = String(asset.filepath).replace(/[^\\/]+$/, newName);
                        }
                        if (asset.path) {
                            asset.path = String(asset.path).replace(/[^\\/]+$/, newName);
                        }
                        if (asset.file_info && typeof asset.file_info === "object") {
                            asset.file_info.filename = newName;
                            if (asset.file_info.filepath) {
                                asset.file_info.filepath = String(asset.file_info.filepath).replace(
                                    /[^\\/]+$/,
                                    newName,
                                );
                            }
                            if (asset.file_info.path) {
                                asset.file_info.path = String(asset.file_info.path).replace(
                                    /[^\\/]+$/,
                                    newName,
                                );
                            }
                        }
                        syncRenderedCardAsset(asset, {
                            filename: asset.filename,
                            filepath: asset.filepath,
                            path: asset.path,
                            file_info: asset.file_info,
                        });
                    }

                    const rendered = findRenderedCard(asset.id);
                    const filenameEl = rendered?.querySelector?.(".mjr-filename");
                    if (filenameEl) filenameEl.textContent = asset.filename || newName;

                    comfyToast(t("toast.fileRenamedSuccess"), "success");
                    triggerBrowserGridReload(gridContainer);
                } catch (error: any) {
                    comfyToast(
                        t("toast.errorRenaming", "Error renaming file: {error}", {
                            error: error?.message || String(error || ""),
                        }),
                        "error",
                    );
                }
            },
            { disabled: !(asset?.id || getAssetFilepath(asset)) },
        ),
    );

    if (isMultiSelected) {
        items.push(
            createItem(
                `Delete ${effectiveSelectionCount} files...`,
                "pi pi-trash",
                getShortcutDisplay("DELETE"),
                async () => {
                    const ok = await confirmDeletion(Number(effectiveSelectionCount) || 0);
                    if (!ok) return;

                    try {
                        let successCount = 0;
                        let errorCount = 0;
                        const deletedIds = [];
                        let deletedByFilepath = 0;
                        const selectionList = selectedAssetsNow.length
                            ? selectedAssetsNow
                            : [asset];

                        for (const selectedAsset of selectionList) {
                            const result = await deleteAsset(selectedAsset);
                            if (result?.ok) {
                                successCount += 1;
                                if (selectedAsset?.id != null) {
                                    deletedIds.push(String(selectedAsset.id));
                                } else {
                                    deletedByFilepath += 1;
                                }
                            } else {
                                errorCount += 1;
                            }
                        }

                        if (deletedIds.length) {
                            const removal = removeAssetsFromGrid(gridContainer, deletedIds);
                            if (panelState && Array.isArray(removal?.selectedIds)) {
                                panelState.selectedAssetIds = removal.selectedIds;
                                panelState.activeAssetId = removal.selectedIds[0] || "";
                            }
                        }
                        if (deletedByFilepath > 0) {
                            triggerBrowserGridReload(gridContainer);
                        }

                        if (errorCount === 0) {
                            comfyToast(
                                t("toast.filesDeletedSuccessN", "{n} files deleted successfully!", {
                                    n: successCount,
                                }),
                                "success",
                            );
                        } else {
                            comfyToast(
                                t(
                                    "toast.filesDeletedPartial",
                                    "{success} files deleted, {failed} failed.",
                                    { success: successCount, failed: errorCount },
                                ),
                                "warning",
                            );
                        }
                    } catch (error: any) {
                        comfyToast(
                            t("toast.errorDeleting", "Error deleting file: {error}", {
                                error: error?.message || String(error || ""),
                            }),
                            "error",
                        );
                    }
                },
            ),
        );
    } else {
        items.push(
            createItem(
                t("ctx.delete", "Delete"),
                "pi pi-trash",
                getShortcutDisplay("DELETE"),
                async () => {
                    const ok = await confirmDeletion(1, asset?.filename);
                    if (!ok) return;
                    try {
                        const result = await deleteAsset(asset);
                        if (!result?.ok) {
                            comfyToast(result?.error || t("toast.fileDeleteFailed"), "error");
                            return;
                        }

                        if (asset?.id) {
                            const removal = removeAssetsFromGrid(gridContainer, [String(asset.id)]);
                            if (panelState && Array.isArray(removal?.selectedIds)) {
                                panelState.selectedAssetIds = removal.selectedIds;
                                panelState.activeAssetId = removal.selectedIds[0] || "";
                            }
                        } else {
                            triggerBrowserGridReload(gridContainer);
                        }
                        comfyToast(t("toast.fileDeletedSuccess"), "success");
                    } catch (error: any) {
                        comfyToast(
                            t("toast.errorDeleting", "Error deleting file: {error}", {
                                error: error?.message || String(error || ""),
                            }),
                            "error",
                        );
                    }
                },
                { disabled: !(asset?.id || getAssetFilepath(asset)) },
            ),
        );
    }

    return items;
}

export function showTagsPopover(x: number, y: number, asset: Record<string, any> | null, onChanged: (() => void) | null): void {
    if (!asset) return;
    openGridTagsPopover({
        x,
        y,
        asset,
        onChanged,
    });
}

export function bindGridContextMenu({ gridContainer, getState = () => ({}) }: { gridContainer?: any; getState?: () => Record<string, any> } = {}): (() => void) | undefined {
    if (!gridContainer) return;
    if (
        gridContainer._mjrGridContextMenuBound &&
        typeof gridContainer._mjrGridContextMenuUnbind === "function"
    ) {
        return gridContainer._mjrGridContextMenuUnbind;
    }

    const handler = async (event: any) => {
        const panelState = (() => {
            try {
                return getState?.() || {};
            } catch {
                return {};
            }
        })();
        const card = safeClosest(event.target, ".mjr-asset-card");

        if (!card) {
            if (!_isBrowserScope(panelState)) return;
            const currentPath = String(gridContainer?.dataset?.mjrSubfolder || "").trim();
            if (!currentPath) return;

            event.preventDefault();
            event.stopPropagation();
            openGridContextMenu({
                x: event.clientX,
                y: event.clientY,
                items: _buildEmptyGridItems({ currentPath, gridContainer }),
            });
            return;
        }

        event.preventDefault();
        event.stopPropagation();

        const asset = (card as any)._mjrAsset;
        if (!asset) return;

        const selection = _getSelectionSnapshot(gridContainer);

        const items = _isFolderAsset(asset)
            ? _buildFolderItems({ asset, gridContainer, panelState })
            : _buildAssetItems({
                  asset,
                  card,
                  gridContainer,
                  panelState,
                  pointerX: event.clientX,
                  pointerY: event.clientY,
                  selection,
              });

        openGridContextMenu({
            x: event.clientX,
            y: event.clientY,
            items,
        });
    };

    try {
        gridContainer.addEventListener("contextmenu", handler);
    } catch (err: any) {
        console.error("[GridContextMenu] Failed to bind:", err);
    }

    gridContainer._mjrGridContextMenuBound = true;
    const unbind = () => {
        try {
            gridContainer.removeEventListener("contextmenu", handler);
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            gridContainer._mjrGridContextMenuBound = false;
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            gridContainer._mjrGridContextMenuUnbind = null;
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            cancelAllRatingUpdates();
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            closeAllGridContextMenus();
        } catch (e: any) {
            console.debug?.(e);
        }
    };
    gridContainer._mjrGridContextMenuUnbind = unbind;
    return unbind;
}
