import { safeAddListener, safeCall } from "./lifecycle.js";
import { comfyToast } from "../../app/toast.js";
import { t } from "../../app/i18n.js";
import { isHotkeysSuspended } from "../panel/controllers/hotkeysState.js";
import { ASSET_TAGS_CHANGED_EVENT } from "../../app/events.js";
import { safeDispatchCustomEvent } from "../../utils/events.js";
import {
    openViewerTagsPopover,
    closeViewerTagsPopover,
    viewerContextMenuState,
} from "../contextmenu/viewerContextMenuState.js";

/**
 * Extract prompt text from asset metadata for clipboard copy.
 */
function extractPromptText(asset: any) {
    if (!asset) return null;
    try {
        // Direct prompt field
        if (typeof asset.prompt === "string" && asset.prompt.trim()) {
            return asset.prompt.trim();
        }
        // From geninfo object
        if (asset.geninfo) {
            const gi = asset.geninfo;
            if (typeof gi.prompt === "string" && gi.prompt.trim()) return gi.prompt.trim();
            if (typeof gi.positive_prompt === "string" && gi.positive_prompt.trim()) {
                let text = gi.positive_prompt.trim();
                if (typeof gi.negative_prompt === "string" && gi.negative_prompt.trim()) {
                    text += "\n\nNegative prompt: " + gi.negative_prompt.trim();
                }
                return text;
            }
        }
        // From metadata_raw (parsed geninfo)
        const raw = asset.metadata_raw;
        if (raw && typeof raw === "object") {
            if (typeof raw.prompt === "string" && raw.prompt.trim()) return raw.prompt.trim();
            const gi = raw.geninfo || raw.GenInfo || raw.generation;
            if (gi && typeof gi === "object") {
                if (typeof gi.prompt === "string" && gi.prompt.trim()) return gi.prompt.trim();
                if (typeof gi.positive_prompt === "string" && gi.positive_prompt.trim()) {
                    let text = gi.positive_prompt.trim();
                    if (typeof gi.negative_prompt === "string" && gi.negative_prompt.trim()) {
                        text += "\n\nNegative prompt: " + gi.negative_prompt.trim();
                    }
                    return text;
                }
            }
        }
        // From raw string metadata (e.g., PNG tEXt parameters)
        if (typeof raw === "string" && raw.includes("Negative prompt:")) {
            // Looks like A1111-style
            return raw.trim();
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    return null;
}

export function installViewerKeyboard({
    overlay,
    _content,
    singleView,
    state,
    VIEWER_MODES,
    computeOneToOneZoom,
    setZoom,
    scheduleOverlayRedraw,
    scheduleApplyGrade,
    syncToolsUIFromState,
    applyDistractionFreeUI,
    navigateViewerAssets,
    closeViewer,
    renderBadges,
    updateAssetRating,
    safeDispatchCustomEvent,
    ASSET_RATING_CHANGED_EVENT,
    probeTooltip,
    loupeWrap,
    getVideoControls,
    lifecycle,
    renderGenInfoPanel,
}: Record<string, any> = {}): Record<string, any> {
    const unsubs = lifecycle?.unsubs || [];

    let ratingDebounceTimer: any = null;
    let ratingPending: any = null;

    const clearRatingDebounce = () => {
        try {
            if (ratingDebounceTimer) clearTimeout(ratingDebounceTimer);
        } catch (e: any) {
            console.debug?.(e);
        }
        ratingDebounceTimer = null;
        ratingPending = null;
    };

    const scheduleRatingUpdate = (assetId: any, rating: any) => {
        clearRatingDebounce();
        ratingPending = { assetId, rating };
        ratingDebounceTimer = setTimeout(async () => {
            const pending = ratingPending;
            ratingDebounceTimer = null;
            ratingPending = null;
            if (!pending?.assetId) return;
            try {
                const result = await updateAssetRating?.(pending.assetId, pending.rating);
                if (!result?.ok) {
                    comfyToast(result?.error || t("toast.ratingUpdateFailed"), "error");
                    return;
                }

                comfyToast(t("toast.ratingSetN", { n: pending.rating }), "success", 1500);

                safeDispatchCustomEvent?.(
                    ASSET_RATING_CHANGED_EVENT,
                    { assetId: String(pending.assetId), rating: pending.rating },
                    { warnPrefix: "[Viewer]" },
                );
            } catch {
                comfyToast(t("toast.ratingUpdateError"), "error");
            }
        }, 300);
    };

    const toggleFullscreen = () => {
        try {
            if (!document.fullscreenElement) {
                overlay?.requestFullscreen?.();
            } else {
                document?.exitFullscreen?.();
            }
        } catch (e: any) {
            console.debug?.(e);
        }
    };

    const handleKeyboard = (e: any) => {
        const consume = () => {
            try {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation?.();
            } catch (e: any) {
                console.debug?.(e);
            }
        };

        const isSingle = state?.mode === VIEWER_MODES?.SINGLE;
        const current = state?.assets?.[state?.currentIndex];

        const isPlayerbarTarget = () => {
            try {
                return Boolean(e?.target?.closest?.(".mjr-viewer-playerbar"));
            } catch {
                return false;
            }
        };

        const isPlayerShortcutKey = () => {
            const key = String(e?.key || "");
            return (
                key === " " ||
                key === "Spacebar" ||
                key === "ArrowLeft" ||
                key === "ArrowRight" ||
                key === "Home" ||
                key === "End" ||
                key === "[" ||
                key === "{" ||
                key === "]" ||
                key === "}" ||
                key === "\\" ||
                key === "|" ||
                key === "i" ||
                key === "I" ||
                key === "o" ||
                key === "O"
            );
        };

        // Check if hotkeys are suspended (e.g. when dialogs/popovers are open)
        if (isHotkeysSuspended()) return;

        try {
            if (overlay?.style?.display === "none") return;
        } catch (e: any) {
            console.debug?.(e);
        }

        // If an input-like element is focused inside the viewer, avoid hijacking typing.
        try {
            const t = e?.target;
            if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) {
                if (isSingle && current?.kind === "video" && isPlayerbarTarget() && isPlayerShortcutKey()) {
                    // Let player shortcuts keep working from the timeline/rate controls.
                } else {
                    if (e.key === "f" || e.key === "F") {
                        consume();
                        toggleFullscreen();
                        return;
                    }

                    if (e.key === "Escape") {
                        consume();
                        if (viewerContextMenuState.tags.open) {
                            closeViewerTagsPopover();
                        } else {
                            safeCall(closeViewer);
                        }
                    }
                    return;
                }
            }
        } catch (e: any) {
            console.debug?.(e);
        }

        const setRatingFromKey = async (key: any) => {
            if (!isSingle) return false;
            if (!current?.id) return false;
            if (
                key !== "0" &&
                key !== "1" &&
                key !== "2" &&
                key !== "3" &&
                key !== "4" &&
                key !== "5"
            )
                return false;
            const rating = key === "0" ? 0 : Number(key);
            if (!Number.isFinite(rating)) return false;

            try {
                current.rating = rating;
                safeCall(renderBadges);
                scheduleRatingUpdate(current.id, rating);
                return true;
            } catch {
                return true;
            }
        };

        const getControls = () => {
            try {
                return getVideoControls?.() || null;
            } catch {
                return null;
            }
        };

        const stepFrame = async (direction: any) => {
            if (!isSingle) return false;
            if (current?.kind !== "video") return false;

            // Prefer the mounted player bar logic (respects In/Out range + step + loop).
            try {
                const controls = getControls();
                if (controls?.stepFrames) {
                    controls.stepFrames(direction);
                    return true;
                }
            } catch (e: any) {
                console.debug?.(e);
            }

            const video = singleView?.querySelector?.("video");
            if (!video) return false;

            try {
                // Pause so arrows behave deterministically.
                video.pause?.();
            } catch (e: any) {
                console.debug?.(e);
            }

            const fpsGuess = 30;
            const step = 1 / fpsGuess;
            const delta = direction * step;

            try {
                const duration = Number(video.duration);
                const next = Math.max(
                    0,
                    Math.min(
                        Number.isFinite(duration) ? duration : Infinity,
                        (video.currentTime || 0) + delta,
                    ),
                );
                video.currentTime = next;
                try {
                    video.dispatchEvent?.(
                        new CustomEvent("mjr:frameStep", { detail: { direction, time: next } }),
                    );
                } catch (e: any) {
                    console.debug?.(e);
                }
                return true;
            } catch {
                return true;
            }
        };

        const changePlaybackRate = (deltaOrAbsolute: any, { absolute = false } = {}) => {
            try {
                const controls = getControls();
                if (!controls) return false;
                if (absolute) {
                    const rate = controls.setPlaybackRate?.(deltaOrAbsolute);
                    if (Number.isFinite(Number(rate))) {
                        state.playbackRate = Number(rate);
                        comfyToast(
                            t("toast.playbackRate", "Playback {rate}x", {
                                rate: Number(rate).toFixed(2),
                            }),
                            "info",
                            1200,
                        );
                        return true;
                    }
                    return false;
                }
                const rate = controls.adjustPlaybackRate?.(deltaOrAbsolute);
                if (Number.isFinite(Number(rate))) {
                    state.playbackRate = Number(rate);
                    comfyToast(
                        t("toast.playbackRate", "Playback {rate}x", {
                            rate: Number(rate).toFixed(2),
                        }),
                        "info",
                        1200,
                    );
                    return true;
                }
                return false;
            } catch {
                return false;
            }
        };

        // Ctrl/Cmd+C: Copy prompt to clipboard (when not focused on an input)
        if ((e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C")) {
            try {
                // Get the prompt from current asset
                const promptText = extractPromptText(current);
                if (promptText) {
                    consume();
                    void navigator.clipboard
                        ?.writeText?.(promptText)
                        .then(() =>
                            comfyToast(
                                t("toast.promptCopied", "Prompt copied to clipboard"),
                                "success",
                                1500,
                            ),
                        )
                        .catch(() =>
                            comfyToast(t("toast.copyFailed", "Copy failed"), "error", 1500),
                        );
                    return;
                }
            } catch (ex: any) {
                console.debug?.(ex);
            }
        }

        // Rating shortcuts (viewer single only). Ignore modifier combos so we can reserve Alt+keys for viewer tools.
        if (
            isSingle &&
            !e.altKey &&
            !e.ctrlKey &&
            !e.metaKey &&
            (e.key === "0" ||
                e.key === "1" ||
                e.key === "2" ||
                e.key === "3" ||
                e.key === "4" ||
                e.key === "5")
        ) {
            consume();
            void setRatingFromKey(e.key);
            return;
        }

        // Frame-by-frame logic is now handled in Arrow shortcuts contextually

        switch (e.key) {
            case "1": {
                // Alt+1 toggles 1:1 pixel zoom (plain "1" is used for rating hotkeys).
                if (!e.altKey) break;
                const z = safeCall(computeOneToOneZoom);
                if (z == null) break;
                consume();
                try {
                    const near = Math.abs((Number(state?.zoom) || 1) - (z as number)) < 0.01;
                    setZoom?.(near ? 1 : z, {
                        clientX: state?._lastPointerX,
                        clientY: state?._lastPointerY,
                    });
                } catch (e: any) {
                    console.debug?.(e);
                }
                break;
            }
            case "g":
            case "G": {
                consume();
                try {
                    state.gridMode = ((Number(state.gridMode) || 0) + 1) % 5;
                } catch (e: any) {
                    console.debug?.(e);
                }
                safeCall(scheduleOverlayRedraw);
                safeCall(syncToolsUIFromState);
                break;
            }
            case "f":
            case "F": {
                consume();
                toggleFullscreen();
                break;
            }
            case "d":
            case "D": {
                consume();
                try {
                    state.genInfoOpen = !state.genInfoOpen;
                } catch (e: any) {
                    console.debug?.(e);
                }
                safeCall(syncToolsUIFromState);
                safeCall(renderGenInfoPanel);
                break;
            }
            case "t":
            case "T": {
                if (!current?.id) break;
                consume();
                openViewerTagsPopover({
                    x: Number(state?._lastPointerX) || Math.round((overlay?.clientWidth || 0) / 2),
                    y: Number(state?._lastPointerY) || Math.round((overlay?.clientHeight || 0) / 2),
                    asset: current,
                    onChanged: ((...args: any[]) => {
                        const tags = args[0];
                        current.tags = tags;
                        safeDispatchCustomEvent(
                            ASSET_TAGS_CHANGED_EVENT,
                            { assetId: String(current.id), tags },
                            { warnPrefix: "[ViewerKeyboard]" },
                        );
                        safeCall(renderBadges);
                    }) as () => void,
                });
                break;
            }
            case "z":
            case "Z": {
                consume();
                try {
                    state.analysisMode = state.analysisMode === "zebra" ? "none" : "zebra";
                } catch (e: any) {
                    console.debug?.(e);
                }
                safeCall(syncToolsUIFromState);
                safeCall(scheduleApplyGrade);
                break;
            }
            case "i":
            case "I": {
                // For video: I sets In point; for other media: toggle pixel probe
                if (isSingle && current?.kind === "video") {
                    const controls = getControls();
                    if (controls?.setInPoint?.()) {
                        consume();
                        comfyToast(t("toast.inPointSet", "In point set"), "info", 1200);
                        break;
                    }
                }
                // Fall through to pixel probe for images
                consume();
                try {
                    state.probeEnabled = !state.probeEnabled;
                } catch (e: any) {
                    console.debug?.(e);
                }
                try {
                    if (!state.probeEnabled) probeTooltip.style.display = "none";
                } catch (e: any) {
                    console.debug?.(e);
                }
                safeCall(syncToolsUIFromState);
                break;
            }
            case "o":
            case "O": {
                // O sets Out point (video only)
                if (isSingle && current?.kind === "video") {
                    const controls = getControls();
                    if (controls?.setOutPoint?.()) {
                        consume();
                        comfyToast(t("toast.outPointSet", "Out point set"), "info", 1200);
                        break;
                    }
                }
                break;
            }
            case "Home": {
                // Home: go to In point (video only)
                if (isSingle && current?.kind === "video") {
                    const controls = getControls();
                    if (controls?.goToIn?.()) {
                        consume();
                        break;
                    }
                }
                break;
            }
            case "End": {
                // End: go to Out point (video only)
                if (isSingle && current?.kind === "video") {
                    const controls = getControls();
                    if (controls?.goToOut?.()) {
                        consume();
                        break;
                    }
                }
                break;
            }
            case "l":
            case "L": {
                consume();
                try {
                    state.loupeEnabled = !state.loupeEnabled;
                } catch (e: any) {
                    console.debug?.(e);
                }
                try {
                    if (!state.loupeEnabled) loupeWrap.style.display = "none";
                } catch (e: any) {
                    console.debug?.(e);
                }
                safeCall(syncToolsUIFromState);
                break;
            }
            case "x":
            case "X": {
                consume();
                try {
                    state.distractionFree = !state.distractionFree;
                } catch (e: any) {
                    console.debug?.(e);
                }
                safeCall(syncToolsUIFromState);
                safeCall(applyDistractionFreeUI);
                safeCall(renderGenInfoPanel);
                break;
            }
            case "c":
            case "C": {
                const p = state?._probe;
                if (!p || p.r == null || p.g == null || p.b == null) break;
                const hex = `#${[p.r, p.g, p.b]
                    .map((v: any) => {
                        const n = Math.max(0, Math.min(255, Number(v) || 0));
                        return n.toString(16).padStart(2, "0");
                    })
                    .join("")}`;
                try {
                    const clip = navigator?.clipboard;
                    if (clip?.writeText) {
                        consume();
                        void clip.writeText(hex).catch(() => {});
                    }
                } catch (e: any) {
                    console.debug?.(e);
                }
                break;
            }
            case " ":
            case "Spacebar": {
                if (isSingle && current?.kind === "video") {
                    const videoEl = singleView?.querySelector?.("video");
                    if (videoEl) {
                        consume();
                        try {
                            const controls = getControls();
                            if (controls?.togglePlay) {
                                controls.togglePlay();
                                break;
                            }
                        } catch (e: any) {
                            console.debug?.(e);
                        }
                        try {
                            if (videoEl.paused) {
                                const p = videoEl.play?.();
                                if (p && typeof p.catch === "function") p.catch(() => {});
                            } else {
                                videoEl.pause?.();
                            }
                        } catch (e: any) {
                            console.debug?.(e);
                        }
                        break;
                    }
                }
                break;
            }
            case "Tab": {
                consume();
                try {
                    if (!overlay?.contains?.(document.activeElement)) overlay?.focus?.();
                } catch (e: any) {
                    console.debug?.(e);
                }
                break;
            }
            case "Escape":
                consume();
                if (viewerContextMenuState.tags.open) {
                    closeViewerTagsPopover();
                } else {
                    safeCall(closeViewer);
                }
                break;
            case "ArrowLeft":
                if (isSingle && e.target?.closest?.(".mjr-viewer-playerbar")) {
                    consume();
                    void stepFrame(-1);
                    break;
                }
                consume();
                safeCall(() => navigateViewerAssets?.(-1));
                break;
            case "ArrowRight":
                if (isSingle && e.target?.closest?.(".mjr-viewer-playerbar")) {
                    consume();
                    void stepFrame(1);
                    break;
                }
                consume();
                safeCall(() => navigateViewerAssets?.(1));
                break;
            case "[":
            case "{": {
                consume();
                if (!changePlaybackRate(-0.25)) {
                    comfyToast(t("toast.playbackVideoOnly"), "warning", 1400);
                }
                break;
            }
            case "]":
            case "}": {
                consume();
                if (!changePlaybackRate(0.25)) {
                    comfyToast(t("toast.playbackVideoOnly"), "warning", 1400);
                }
                break;
            }
            case "\\":
            case "|": {
                consume();
                if (!changePlaybackRate(1, { absolute: true })) {
                    comfyToast(t("toast.playbackVideoOnly"), "warning", 1400);
                }
                break;
            }
            case "+":
            case "=":
                consume();
                try {
                    setZoom?.((Number(state?.zoom) || 1) + 0.25, {
                        clientX: state?._lastPointerX,
                        clientY: state?._lastPointerY,
                    });
                } catch (e: any) {
                    console.debug?.(e);
                }
                break;
            case "-":
            case "_":
                consume();
                try {
                    setZoom?.((Number(state?.zoom) || 1) - 0.25, {
                        clientX: state?._lastPointerX,
                        clientY: state?._lastPointerY,
                    });
                } catch (e: any) {
                    console.debug?.(e);
                }
                break;
        }
    };

    let keyUnsub: any = null;
    const bind = () => {
        try {
            if (keyUnsub) return;
            keyUnsub = safeAddListener(window, "keydown", handleKeyboard, true);
        } catch (e: any) {
            console.debug?.(e);
        }
    };
    const unbind = () => {
        try {
            safeCall(keyUnsub);
        } catch (e: any) {
            console.debug?.(e);
        }
        keyUnsub = null;
    };

    unsubs.push(() => clearRatingDebounce());
    unsubs.push(() => unbind());

    return {
        bind,
        unbind,
        dispose: () => {
            clearRatingDebounce();
            unbind();
        },
    };
}
