/**
 * Grid Keyboard Shortcuts - Keyboard navigation and actions for asset grid
 *
 * Shortcuts are designed to be:
 * - Mnemonic (easy to remember)
 * - Standard (consistent with Lightroom, Windows, macOS)
 * - Non-conflicting with browser/ComfyUI defaults
 *
 * @see docs/SHORTCUTS.md for full documentation
 */

import { comfyToast } from "../../app/toast.js";
import { t } from "../../app/i18n.js";
import { comfyPrompt } from "../../app/dialogs.js";
import {
    openInFolder,
    updateAssetRating,
    deleteAsset,
    renameAsset,
    removeFilepathsFromCollection,
} from "../../api/client.js";
import { buildDownloadURL } from "../../api/endpoints.js";
import { ASSET_RATING_CHANGED_EVENT } from "../../app/events.js";
import { safeDispatchCustomEvent } from "../../utils/events.js";
import { showAddToCollectionMenu } from "../collections/contextmenu/addToCollectionMenu.js";
import { normalizeRenameFilename, validateFilename } from "../../utils/filenames.js";
import { getHotkeysState, isHotkeysSuspended } from "../panel/controllers/hotkeysState.js";
import {
    clearActiveGridContainer,
    getActiveGridContainer,
    setActiveGridContainer,
} from "../panel/panelRuntimeRefs.js";

/**
 * Keyboard shortcut definitions
 * Format: { key, ctrl, shift, alt, action, description }
 */
const GRID_SHORTCUTS = {
    // Viewing
    OPEN_VIEWER: { key: "Enter", description: "Open selected in viewer" },
    OPEN_VIEWER_ALT: { key: " ", description: "Open selected in viewer" },
    OPEN_FLOATING_VIEWER: { key: "v", description: "Open selected in Floating Viewer" },
    METADATA_PANEL: { key: "d", description: "Show metadata panel (Details)" },
    COMPARE_AB: { key: "c", description: "Compare A/B (2 selected)" },
    SIDE_BY_SIDE: { key: "g", description: "Side-by-side Grid view (2-4 selected)" },

    // Rating (Lightroom/Bridge standard)
    RATING_1: { key: "1", description: "Set rating 1 star" },
    RATING_2: { key: "2", description: "Set rating 2 stars" },
    RATING_3: { key: "3", description: "Set rating 3 stars" },
    RATING_4: { key: "4", description: "Set rating 4 stars" },
    RATING_5: { key: "5", description: "Set rating 5 stars" },
    RATING_RESET: { key: "0", description: "Reset rating" },

    // Organization
    EDIT_TAGS: { key: "t", description: "Edit Tags" },
    ADD_TO_COLLECTION: { key: "b", description: "Add to collection (Bookmark)" },
    REMOVE_FROM_COLLECTION: { key: "b", shift: true, description: "Remove from collection" },

    // File operations
    COPY_PATH: { key: "c", ctrl: true, shift: true, description: "Copy file path" },
    DOWNLOAD: { key: "s", description: "Download file" },
    RENAME: { key: "F2", description: "Rename file" },
    DELETE: { key: "Delete", description: "Delete file" },
    DELETE_ALT: { key: "Backspace", description: "Delete file" },
    OPEN_IN_FOLDER: { key: "e", ctrl: true, shift: true, description: "Open in folder (Explorer)" },

    // Selection
    SELECT_ALL: { key: "a", ctrl: true, description: "Select all" },
    DESELECT_ALL: { key: "d", ctrl: true, description: "Deselect all" },

    // Navigation
    NAV_UP: { key: "ArrowUp", description: "Navigate up" },
    NAV_DOWN: { key: "ArrowDown", description: "Navigate down" },
    NAV_LEFT: { key: "ArrowLeft", description: "Navigate left" },
    NAV_RIGHT: { key: "ArrowRight", description: "Navigate right" },
};

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

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

/**
 * Format shortcut for display in context menu
 * @param {Object} shortcut - Shortcut definition
 * @returns {string} Formatted shortcut string (e.g., "Ctrl+Shift+C")
 */
function formatShortcut(shortcut: any) {
    if (!shortcut) return "";
    const parts = [];
    if (shortcut.ctrl) parts.push("Ctrl");
    if (shortcut.shift) parts.push("Shift");
    if (shortcut.alt) parts.push("Alt");

    let key = shortcut.key;
    // Format special keys
    if (key === " ") key = "Space";
    if (key === "Delete") key = "Del";
    if (key === "ArrowUp") key = "<-‘";
    if (key === "ArrowDown") key = "<-“";
    if (key === "ArrowLeft") key = "<-";
    if (key === "ArrowRight") key = "->";

    parts.push(key.length === 1 ? key.toUpperCase() : key);
    return parts.join("+");
}

/**
 * Check if a keyboard event matches a shortcut definition
 * @param {KeyboardEvent} e - Keyboard event
 * @param {Object} shortcut - Shortcut definition
 * @returns {boolean} True if event matches shortcut
 */
function matchesShortcut(e: any, shortcut: any) {
    if (!shortcut || !e) return false;

    const keyMatches = e.key.toLowerCase() === shortcut.key.toLowerCase() || e.key === shortcut.key;
    const ctrlMatches = !!shortcut.ctrl === (e.ctrlKey || e.metaKey);
    const shiftMatches = !!shortcut.shift === e.shiftKey;
    const altMatches = !!shortcut.alt === e.altKey;

    return keyMatches && ctrlMatches && shiftMatches && altMatches;
}

/**
 * Rating debounce state
 */
const _ratingDebounceTimers = new Map();

function setRating(asset: any, rating: any, onChanged: any) {
    const assetId = asset?.id;
    if (!assetId) return;
    const id = String(assetId);

    try {
        const prev = _ratingDebounceTimers.get(id);
        if (prev) clearTimeout(prev);
    } catch (e: any) {
        console.debug?.(e);
    }

    // Optimistic UI
    try {
        asset.rating = rating;
        onChanged?.();
    } catch (e: any) {
        console.debug?.(e);
    }

    const timerId = setTimeout(async () => {
        try {
            _ratingDebounceTimers.delete(id);
            const result = await updateAssetRating(assetId, rating);
            if (!result?.ok) {
                comfyToast(result?.error || t("toast.ratingUpdateFailed"), "error");
                return;
            }
            comfyToast(t("toast.ratingSetN", { n: rating }), "success", 1500);
            safeDispatchCustomEvent(
                ASSET_RATING_CHANGED_EVENT,
                { assetId: String(assetId), rating },
                { warnPrefix: "[GridKeyboard]" },
            );
        } catch (err: any) {
            console.error("[GridKeyboard] Rating update failed:", err);
            comfyToast(t("toast.ratingUpdateError"), "error");
        }
    }, 350);

    _ratingDebounceTimers.set(id, timerId);
}

/**
 * Install keyboard shortcuts for the asset grid
 *
 * @param {Object} options - Configuration options
 * @param {HTMLElement} options.gridContainer - Grid container element
 * @param {Function} options.getState - Function to get panel state
 * @param {Function} options.getSelectedAssets - Function to get selected assets
 * @param {Function} options.getActiveAsset - Function to get the active/focused asset
 * @param {Function} options.onOpenDetails - Callback to open details panel
 * @param {Function} options.onOpenTagsEditor - Callback to open tags editor
 * @param {Function} options.onSelectionChanged - Callback when selection changes
 * @param {Function} options.onAssetChanged - Callback when asset is modified
 * @returns {Object} Object with bind, unbind, and dispose methods
 */
export function installGridKeyboard({
    gridContainer,
    getState = () => ({}),
    getSelectedAssets = () => [],
    getActiveAsset = () => null,
    onOpenDetails = () => {},
    onOpenTagsEditor = () => {},
    onSelectionChanged = () => {},
    onAssetChanged = () => {},
}: { gridContainer?: any; getState?: () => Record<string, any>; getSelectedAssets?: () => any[]; getActiveAsset?: () => any; onOpenDetails?: (asset?: any) => void; onOpenTagsEditor?: (asset?: any) => void; onSelectionChanged?: () => void; onAssetChanged?: (asset?: any) => void } = {}): any {
    if (!gridContainer) return { bind: () => {}, unbind: () => {}, dispose: () => {} };

    let keydownHandler: any = null;
    let keyupHandler: any = null;
    let dragstartHandler: any = null;
    let activateGridHandler: any = null;
    let bound = false;
    let pendingDownloadShortcut = false;
    const openInViewer = ({ assets = [] as any[], index = 0, mode = "" } = {}) => {
        void loadViewerOpenRequestModule()
            .then((mod) => mod.requestViewerOpen({ assets, index, mode }))
            .catch((e) => console.debug?.(e));
    };

    const markGridActive = () => {
        try {
            setActiveGridContainer(gridContainer);
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            (window as any).__MJR_LAST_SELECTION_GRID__ = gridContainer;
        } catch (e: any) {
            console.debug?.(e);
        }
    };

    const getSelection = () => {
        try {
            return getSelectedAssets() || [];
        } catch {
            return [];
        }
    };

    const getRenderedCards = () => {
        try {
            if (typeof gridContainer?._mjrGetRenderedCards === "function") {
                const cards = gridContainer._mjrGetRenderedCards();
                if (Array.isArray(cards)) return cards;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            return Array.from(gridContainer.querySelectorAll(".mjr-asset-card"));
        } catch {
            return [];
        }
    };

    const getActive = () => {
        try {
            const active = getActiveAsset();
            if (active) return active;
            // Fallback: first selected
            const selected = getSelection();
            return selected[0] || null;
        } catch {
            return null;
        }
    };

    const downloadAsset = (asset: any) => {
        if (!asset?.filepath) return false;
        const url = buildDownloadURL(asset.filepath);
        const link = document.createElement("a");
        link.href = url;
        link.download = asset.filename || "download";
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        comfyToast(
            t("toast.downloadingFile", "Downloading {filename}...", {
                filename: asset.filename,
            }),
            "info",
            3000,
        );
        return true;
    };

    const getAllAssets = () => {
        try {
            if (Array.isArray(getState?.()?.assets) && getState().assets.length)
                return getState().assets;
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            if (typeof gridContainer?._mjrGetAssets === "function") {
                const list = gridContainer._mjrGetAssets();
                if (Array.isArray(list) && list.length) return list;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            return getRenderedCards()
                .map((card: any) => card?._mjrAsset)
                .filter(Boolean);
        } catch {
            return [];
        }
    };

    const getGridColumns = () => {
        try {
            const cards = getRenderedCards();
            if (cards.length >= 2) {
                const firstTop = Math.round(cards[0].getBoundingClientRect().top);
                const sameRow = cards.filter((c: any) => {
                    try {
                        return Math.abs(Math.round(c.getBoundingClientRect().top) - firstTop) <= 4;
                    } catch {
                        return false;
                    }
                });
                if (sameRow.length >= 1) return Math.max(1, sameRow.length);
            }
            if (cards.length >= 1) {
                const cardRect = cards[0].getBoundingClientRect();
                const hostRect = gridContainer.getBoundingClientRect();
                const cardW = Math.max(1, Math.round(cardRect.width));
                const hostW = Math.max(1, Math.round(hostRect.width));
                return Math.max(1, Math.floor(hostW / cardW));
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        return 1;
    };

    const selectByIds = (ids: any, activeId: any) => {
        const list = Array.isArray(ids) ? ids.map(String).filter(Boolean) : [];
        const active = String(activeId || list[0] || "");
        try {
            if (typeof gridContainer?._mjrSetSelection === "function") {
                gridContainer._mjrSetSelection(list, active);
            } else {
                gridContainer.dataset.mjrSelectedAssetIds = JSON.stringify(list);
                gridContainer.dataset.mjrSelectedAssetId = active;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
    };

    const scrollToAssetId = (assetId: any) => {
        const id = String(assetId || "").trim();
        if (!id) return;
        // Prefer VirtualGrid-aware scroll (calculates position from index, works for off-screen items)
        try {
            if (typeof gridContainer?._mjrScrollToAssetId === "function") {
                gridContainer._mjrScrollToAssetId(id);
                return;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        // Fallback: DOM-based scroll (only works if the card is already rendered)
        try {
            const esc =
                typeof CSS !== "undefined" && CSS?.escape
                    ? CSS.escape(id)
                    : id.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1");
            const card = gridContainer.querySelector(`.mjr-asset-card[data-mjr-asset-id="${esc}"]`);
            card?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
        } catch (e: any) {
            console.debug?.(e);
        }
    };

    const handleGridNavigation = (e: any) => {
        const key = String(e.key || "");
        const isArrow =
            key === "ArrowLeft" || key === "ArrowRight" || key === "ArrowUp" || key === "ArrowDown";
        const isEdge = key === "Home" || key === "End";
        if (!isArrow && !isEdge) return false;
        if (e.ctrlKey || e.altKey || e.metaKey) return false;

        const assets = getAllAssets();
        if (!Array.isArray(assets) || assets.length === 0) return false;

        const activeAsset = getActive();
        const currentId = String(
            activeAsset?.id || gridContainer?.dataset?.mjrSelectedAssetId || "",
        );
        let currentIndex = assets.findIndex((a: any) => String(a?.id || "") === currentId);
        const hadSelection = currentIndex >= 0;
        if (currentIndex < 0) currentIndex = 0;

        const cols = Math.max(1, getGridColumns());
        let nextIndex = currentIndex;
        if (key === "ArrowLeft") nextIndex -= 1;
        else if (key === "ArrowRight") nextIndex += 1;
        else if (key === "ArrowUp") nextIndex -= cols;
        else if (key === "ArrowDown") nextIndex += cols;
        else if (key === "Home") nextIndex = 0;
        else if (key === "End") nextIndex = assets.length - 1;

        nextIndex = Math.max(0, Math.min(assets.length - 1, nextIndex));
        // If nothing is selected yet, first arrow/home/end should still select an item.
        if (hadSelection && nextIndex === currentIndex && key !== "Home" && key !== "End")
            return false;

        const nextId = String(assets[nextIndex]?.id || "");
        if (!nextId) return false;

        markGridActive();

        if (e.shiftKey) {
            let anchorId = String(gridContainer?._mjrSelectionAnchorId || "");
            if (!anchorId) anchorId = currentId || nextId;
            const anchorIndex = assets.findIndex((a: any) => String(a?.id || "") === anchorId);
            const from = anchorIndex >= 0 ? anchorIndex : currentIndex;
            const lo = Math.min(from, nextIndex);
            const hi = Math.max(from, nextIndex);
            const rangeIds = assets
                .slice(lo, hi + 1)
                .map((a: any) => String(a?.id || ""))
                .filter(Boolean);
            selectByIds(rangeIds, nextId);
        } else {
            gridContainer._mjrSelectionAnchorId = nextId;
            selectByIds([nextId], nextId);
        }

        scrollToAssetId(nextId);
        try {
            onSelectionChanged?.();
        } catch (e: any) {
            console.debug?.(e);
        }
        return true;
    };

    const handleKeydown = async (e: any) => {
        // Skip if typing in input
        const target = e.target;
        if (
            target &&
            (target.tagName === "INPUT" ||
                target.tagName === "TEXTAREA" ||
                target.isContentEditable)
        ) {
            return;
        }

        // Skip if hotkeys suspended (dialog open, etc.)
        if (isHotkeysSuspended()) return;
        // Viewer must have priority over grid shortcuts.
        if (getHotkeysState().scope === "viewer") return;

        // Skip if grid not visible/focused
        // Allow if focus is:
        // 1. Inside grid (contains active)
        // 2. On grid parent wrapper (active contains grid)
        // 3. On body, but only for the active grid
        // 4. On a card (explicit check though covered by 1 usually)
        if (document.activeElement !== document.body) {
            const active = document.activeElement;
            const inGrid = gridContainer.contains(active);
            const wrapsGrid = active && active.contains && active.contains(gridContainer);
            const isCard = active?.closest?.(".mjr-asset-card");

            if (!inGrid && !wrapsGrid && !isCard) {
                return;
            }
        } else {
            const activeGrid = getActiveGridContainer();
            if (activeGrid && activeGrid !== gridContainer) {
                return;
            }
        }

        const consume = () => {
            e.preventDefault();
            e.stopPropagation();
        };

        // Grid navigation (arrows + Home/End, with Shift range extension)
        try {
            if (handleGridNavigation(e)) {
                consume();
                return;
            }
        } catch (e: any) {
            console.debug?.(e);
        }

        const selected = getSelection();
        const asset = getActive();
        const state = getState();

        if (matchesShortcut(e, GRID_SHORTCUTS.DOWNLOAD)) {
            if (asset?.filepath) {
                consume();
                pendingDownloadShortcut = true;
                return;
            }
        }

        // Rating shortcuts (1-5, 0 for reset) - handled by dedicated rating controller when enabled.
        const ratingHotkeysActive = !!getHotkeysState().ratingHotkeysActive;
        if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) {
            if (!ratingHotkeysActive && matchesShortcut(e, GRID_SHORTCUTS.RATING_1)) {
                if (asset?.id) {
                    consume();
                    setRating(asset, 1, onAssetChanged);
                    return;
                }
            }
            if (!ratingHotkeysActive && matchesShortcut(e, GRID_SHORTCUTS.RATING_2)) {
                if (asset?.id) {
                    consume();
                    setRating(asset, 2, onAssetChanged);
                    return;
                }
            }
            if (!ratingHotkeysActive && matchesShortcut(e, GRID_SHORTCUTS.RATING_3)) {
                if (asset?.id) {
                    consume();
                    setRating(asset, 3, onAssetChanged);
                    return;
                }
            }
            if (!ratingHotkeysActive && matchesShortcut(e, GRID_SHORTCUTS.RATING_4)) {
                if (asset?.id) {
                    consume();
                    setRating(asset, 4, onAssetChanged);
                    return;
                }
            }
            if (!ratingHotkeysActive && matchesShortcut(e, GRID_SHORTCUTS.RATING_5)) {
                if (asset?.id) {
                    consume();
                    setRating(asset, 5, onAssetChanged);
                    return;
                }
            }
            if (!ratingHotkeysActive && matchesShortcut(e, GRID_SHORTCUTS.RATING_RESET)) {
                if (asset?.id) {
                    consume();
                    setRating(asset, 0, onAssetChanged);
                    return;
                }
            }
        }

        // Open Viewer (Enter or Space)
        if (
            matchesShortcut(e, GRID_SHORTCUTS.OPEN_VIEWER) ||
            matchesShortcut(e, GRID_SHORTCUTS.OPEN_VIEWER_ALT)
        ) {
            if (selected.length > 0) {
                consume();
                try {
                    if (selected.length >= 2 && selected.length <= 4) {
                        openInViewer({ assets: selected, index: 0 });
                    } else {
                        // Open all assets from grid, starting at active
                        let allAssets: any[] = [];
                        if (Array.isArray(state?.assets) && state.assets.length) {
                            allAssets = state.assets;
                        } else if (typeof gridContainer?._mjrGetAssets === "function") {
                            const list = gridContainer._mjrGetAssets();
                            if (Array.isArray(list) && list.length) allAssets = list;
                        } else {
                            const cards = getRenderedCards();
                            allAssets = cards.map((card: any) => card._mjrAsset).filter(Boolean);
                        }
                        const idx = allAssets.findIndex((a: any) => String(a?.id) === String(asset?.id));
                        openInViewer({ assets: allAssets, index: Math.max(0, idx) });
                    }
                } catch (err: any) {
                    console.error("[GridKeyboard] Open viewer failed:", err);
                }
                return;
            }
        }

        // Metadata panel (D)
        if (matchesShortcut(e, GRID_SHORTCUTS.METADATA_PANEL)) {
            if (asset) {
                consume();
                onOpenDetails(asset);
                return;
            }
        }

        // Compare A/B (C) - 2 selected only
        if (matchesShortcut(e, GRID_SHORTCUTS.COMPARE_AB)) {
            if (selected.length === 2) {
                consume();
                try {
                    openInViewer({ assets: selected, index: 0, mode: "ab" });
                } catch (e: any) {
                    console.debug?.(e);
                }
                return;
            }
        }

        // Side-by-side (G) - 2-4 selected
        if (matchesShortcut(e, GRID_SHORTCUTS.SIDE_BY_SIDE)) {
            if (selected.length >= 2 && selected.length <= 4) {
                consume();
                try {
                    openInViewer({ assets: selected, index: 0, mode: "sidebyside" });
                } catch (e: any) {
                    console.debug?.(e);
                }
                return;
            }
        }

        // Edit Tags (T)
        if (matchesShortcut(e, GRID_SHORTCUTS.EDIT_TAGS)) {
            if (asset?.id) {
                consume();
                onOpenTagsEditor(asset);
                return;
            }
        }

        // Add to collection (B)
        if (matchesShortcut(e, GRID_SHORTCUTS.ADD_TO_COLLECTION)) {
            if (selected.length > 0 || asset) {
                consume();
                const list = selected.length > 0 ? selected : [asset];
                // Show menu at center of grid
                const rect = gridContainer.getBoundingClientRect();
                await showAddToCollectionMenu({
                    x: rect.left + rect.width / 2,
                    y: rect.top + rect.height / 2,
                    assets: list,
                });
                return;
            }
        }

        // Remove from collection (Shift+B)
        if (matchesShortcut(e, GRID_SHORTCUTS.REMOVE_FROM_COLLECTION)) {
            const collectionId = state?.collectionId;
            if (collectionId && (selected.length > 0 || asset)) {
                consume();
                try {
                    const list = selected.length > 0 ? selected : [asset];
                    const filepaths = list
                        .map((a: any) =>
                            String(a?.filepath || a?.path || a?.file_info?.filepath || "").trim(),
                        )
                        .filter(Boolean);
                    if (!filepaths.length) {
                        comfyToast(
                            t(
                                "toast.removeFromCollectionFailed",
                                "No valid files to remove from collection",
                            ),
                            "warning",
                        );
                        return;
                    }
                    const res = await removeFilepathsFromCollection(collectionId, filepaths);
                    if (!res?.ok) {
                        comfyToast(
                            res?.error ||
                                t(
                                    "toast.removeFromCollectionFailed",
                                    "Failed to remove from collection",
                                ),
                            "error",
                        );
                        return;
                    }
                    try {
                        window.dispatchEvent(
                            new CustomEvent("mjr:reload-grid", {
                                detail: { reason: "remove-from-collection" },
                            }),
                        );
                    } catch (e: any) {
                        console.debug?.(e);
                    }
                    comfyToast(
                        t("toast.removedFromCollection", "Removed from collection"),
                        "success",
                        1400,
                    );
                } catch (err: any) {
                    comfyToast(
                        t(
                            "toast.removeFromCollectionError",
                            "Error removing from collection: {error}",
                            { error: err?.message || String(err || "") },
                        ),
                        "error",
                    );
                }
                return;
            }
        }

        // Copy file path (Ctrl+Shift+C)
        if (matchesShortcut(e, GRID_SHORTCUTS.COPY_PATH)) {
            if (asset?.filepath) {
                consume();
                try {
                    await navigator.clipboard.writeText(asset.filepath);
                    comfyToast(t("toast.pathCopied"), "success", 2000);
                } catch {
                    comfyToast(t("toast.pathCopyFailed"), "error");
                }
                return;
            }
        }

        // Open in Folder (Ctrl+Shift+E)
        if (matchesShortcut(e, GRID_SHORTCUTS.OPEN_IN_FOLDER)) {
            if (asset?.id) {
                consume();
                const res = await openInFolder(asset.id);
                if (!res?.ok) {
                    comfyToast(res?.error || t("toast.openFolderFailed"), "error");
                } else {
                    comfyToast(t("toast.openedInFolder"), "info", 2000);
                }
                return;
            }
        }

        // Rename (F2)
        if (matchesShortcut(e, GRID_SHORTCUTS.RENAME)) {
            if (asset?.id) {
                consume();
                const currentName = asset.filename || "";
                const rawInput = await comfyPrompt(
                    t("dialog.rename.title", "Rename file"),
                    currentName,
                );
                const newName = normalizeRenameFilename(rawInput, currentName);
                if (newName && newName !== currentName) {
                    const validation = validateFilename(newName);
                    if (!validation.valid) {
                        comfyToast(validation.reason, "error");
                        return;
                    }
                    try {
                        const result = await renameAsset(asset, newName);
                        if (result?.ok) {
                            const fresh = result?.data?.asset;
                            if (fresh && typeof fresh === "object") {
                                Object.assign(asset, fresh);
                            } else {
                                asset.filename = newName;
                                asset.filepath = 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,
                                        );
                                }
                            }
                            comfyToast(t("toast.fileRenamedSuccess"), "success");
                            try {
                                window.dispatchEvent(
                                    new CustomEvent("mjr:reload-grid", {
                                        detail: { reason: "rename-keyboard" },
                                    }),
                                );
                            } catch (e: any) {
                                console.debug?.(e);
                            }
                            onAssetChanged();
                        } else {
                            comfyToast(result?.error || t("toast.fileRenameFailed"), "error");
                        }
                    } catch (err: any) {
                        comfyToast(
                            t("toast.errorRenaming", "Error renaming file: {error}", {
                                error: err?.message || String(err || ""),
                            }),
                            "error",
                        );
                    }
                }
                return;
            }
        }

        // Delete (Delete/Backspace)
        if (
            matchesShortcut(e, GRID_SHORTCUTS.DELETE) ||
            matchesShortcut(e, GRID_SHORTCUTS.DELETE_ALT)
        ) {
            if (asset?.id || selected.length > 0) {
                consume();
                const toDelete = selected.length > 0 ? selected : [asset];
                let successCount = 0;
                let errorCount = 0;
                const deletedIds: any[] = [];

                for (const a of toDelete) {
                    if (!a?.id) continue;
                    try {
                        const result = await deleteAsset(a.id);
                        if (result?.ok) {
                            successCount++;
                            deletedIds.push(String(a.id));
                        } else {
                            errorCount++;
                        }
                    } catch {
                        errorCount++;
                    }
                }

                if (deletedIds.length) {
                    try {
                        const removeFn = gridContainer?._mjrRemoveAssets;
                        if (typeof removeFn === "function") {
                            removeFn(deletedIds);
                        } else {
                            for (const id of deletedIds) {
                                const card = gridContainer.querySelector(
                                    `[data-mjr-asset-id="${id}"]`,
                                );
                                card?.remove?.();
                            }
                        }
                    } catch (e: any) {
                        console.debug?.(e);
                    }
                }

                if (errorCount === 0) {
                    comfyToast(
                        successCount === 1
                            ? t("toast.filesDeletedShort", "{n} files deleted", { n: successCount })
                            : t("toast.filesDeletedShort", "{n} files deleted", {
                                  n: successCount,
                              }),
                        "success",
                    );
                } else {
                    comfyToast(
                        t("toast.filesDeletedShortPartial", "{success} deleted, {failed} failed", {
                            success: successCount,
                            failed: errorCount,
                        }),
                        "warning",
                    );
                }
                try {
                    if (typeof gridContainer?._mjrSetSelection === "function") {
                        const remaining = selected
                            .filter((a: any) => !deletedIds.includes(String(a?.id || "")))
                            .map((a: any) => String(a?.id || ""))
                            .filter(Boolean);
                        gridContainer._mjrSetSelection(remaining, remaining[0] || "");
                    }
                } catch (e: any) {
                    console.debug?.(e);
                }
                onSelectionChanged();
                return;
            }
        }

        // Select All (Ctrl+A)
        if (matchesShortcut(e, GRID_SHORTCUTS.SELECT_ALL)) {
            consume();
            let ids: any[] = [];
            try {
                if (Array.isArray(state?.assets) && state.assets.length) {
                    ids = state.assets.map((a: any) => String(a?.id || "")).filter(Boolean);
                } else if (typeof gridContainer?._mjrGetAssets === "function") {
                    const list = gridContainer._mjrGetAssets();
                    if (Array.isArray(list))
                        ids = list.map((a: any) => String(a?.id || "")).filter(Boolean);
                }
            } catch (e: any) {
                console.debug?.(e);
            }
            if (!ids.length) {
                try {
                    ids = getRenderedCards()
                        .map((card: any) => String(card?._mjrAsset?.id || ""))
                        .filter(Boolean);
                } catch (e: any) {
                    console.debug?.(e);
                }
            }
            try {
                if (typeof gridContainer?._mjrSetSelection === "function") {
                    gridContainer._mjrSetSelection(ids, ids[0] || "");
                } else {
                    const cards = getRenderedCards();
                    cards.forEach((card: any) => {
                        card.classList.add("is-selected");
                        card.setAttribute("aria-selected", "true");
                    });
                    gridContainer.dataset.mjrSelectedAssetIds = JSON.stringify(ids);
                    gridContainer.dataset.mjrSelectedAssetId = String(ids[0] || "");
                }
            } catch (e: any) {
                console.debug?.(e);
            }
            onSelectionChanged();
            return;
        }

        // Deselect All (Ctrl+D)
        if (matchesShortcut(e, GRID_SHORTCUTS.DESELECT_ALL)) {
            consume();
            try {
                if (typeof gridContainer?._mjrSetSelection === "function") {
                    gridContainer._mjrSetSelection([], "");
                } else {
                    const cards = getRenderedCards().filter((card: any) =>
                        card?.classList?.contains?.("is-selected"),
                    );
                    cards.forEach((card: any) => {
                        card.classList.remove("is-selected");
                        card.setAttribute("aria-selected", "false");
                    });
                    delete gridContainer.dataset.mjrSelectedAssetIds;
                    delete gridContainer.dataset.mjrSelectedAssetId;
                }
            } catch (e: any) {
                console.debug?.(e);
            }
            onSelectionChanged();
            return;
        }
    };

    const handleKeyup = (e: any) => {
        if (String(e?.key || "").toLowerCase() !== "s") return;
        if (!pendingDownloadShortcut) return;
        pendingDownloadShortcut = false;
        if (isHotkeysSuspended()) return;
        if (getHotkeysState().scope === "viewer") return;
        const asset = getActive();
        if (!asset?.filepath) return;
        e.preventDefault?.();
        e.stopPropagation?.();
        e.stopImmediatePropagation?.();
        downloadAsset(asset);
    };

    const handleDragStart = () => {
        pendingDownloadShortcut = false;
    };

    const bind = () => {
        if (bound) return;
        keydownHandler = handleKeydown;
        keyupHandler = handleKeyup;
        dragstartHandler = handleDragStart;
        activateGridHandler = () => markGridActive();
        document.addEventListener("keydown", keydownHandler, true);
        document.addEventListener("keyup", keyupHandler, true);
        document.addEventListener("dragstart", dragstartHandler, true);
        gridContainer.addEventListener("pointerdown", activateGridHandler, true);
        gridContainer.addEventListener("focusin", activateGridHandler, true);
        bound = true;
    };

    const unbind = () => {
        if (!bound || !keydownHandler) return;
        document.removeEventListener("keydown", keydownHandler, true);
        if (keyupHandler) document.removeEventListener("keyup", keyupHandler, true);
        if (dragstartHandler) document.removeEventListener("dragstart", dragstartHandler, true);
        if (activateGridHandler) {
            gridContainer.removeEventListener("pointerdown", activateGridHandler, true);
            gridContainer.removeEventListener("focusin", activateGridHandler, true);
        }
        keydownHandler = null;
        keyupHandler = null;
        dragstartHandler = null;
        activateGridHandler = null;
        pendingDownloadShortcut = false;
        try {
            clearActiveGridContainer(gridContainer);
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            if ((window as any).__MJR_LAST_SELECTION_GRID__ === gridContainer) {
                delete (window as any).__MJR_LAST_SELECTION_GRID__;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        bound = false;
        for (const t of _ratingDebounceTimers.values()) {
            try {
                clearTimeout(t);
            } catch (e: any) {
                console.debug?.(e);
            }
        }
        _ratingDebounceTimers.clear();
    };

    const dispose = () => {
        unbind();
        // Clear pending timers
        for (const t of _ratingDebounceTimers.values()) {
            try {
                clearTimeout(t);
            } catch (e: any) {
                console.debug?.(e);
            }
        }
        _ratingDebounceTimers.clear();
    };

    return { bind, unbind, dispose };
}

/**
 * Get shortcut string for a specific action (for display in context menus)
 * @param {string} actionName - Name of the action (e.g., "OPEN_VIEWER", "METADATA_PANEL")
 * @returns {string} Formatted shortcut string or empty string
 */
export function getShortcutDisplay(actionName: string): string {
    const shortcut = (GRID_SHORTCUTS as Record<string, any>)[actionName];
    return shortcut ? formatShortcut(shortcut) : "";
}
