import { EVENTS } from "../../app/events.js";
import { get, getAssetMetadata, vectorFindSimilar } from "../../api/client.js";
import { buildListURL, buildNodeContextMembersURL } from "../../api/endpoints.js";
import { comfyToast } from "../../app/toast.js";
import { t } from "../../app/i18n.js";

const STACK_GROUP_RENDER_LIMIT = 500;

function isNodeContextRouteMissing(res: any) {
    return (
        Number(res?.status || 0) === 404 &&
        String(res?.error || "")
            .toLowerCase()
            .includes("non-json")
    );
}

function limitStackGroupMembers(list: any[], title = "") {
    const items = Array.isArray(list) ? list : [];
    if (items.length <= STACK_GROUP_RENDER_LIMIT) {
        return { list: items, title, truncated: false, total: items.length };
    }
    return {
        list: items.slice(0, STACK_GROUP_RENDER_LIMIT),
        title: `${String(title || "").trim() || "Generation group"} - showing first ${STACK_GROUP_RENDER_LIMIT}/${items.length}`,
        truncated: true,
        total: items.length,
    };
}

function buildGroupSourceId(detail: Record<string, any> = {}) {
    const stackId = String(detail?.stackId || detail?.stack_id || "").trim();
    if (stackId) return `stack:${stackId}`;
    const assetId = String(detail?.asset?.id || "").trim();
    if (detail?.isDupGroup) return assetId ? `duplicates:${assetId}` : "duplicates";
    return assetId ? `group:${assetId}` : "group";
}

function readAssetField(asset: any, key: string): string {
    return String(asset?.[key] ?? asset?.file_info?.[key] ?? "").trim();
}

/**
 * Binds the "Find Similar" button and the stack-group open event.
 *
 * Both interactions build a virtual asset list from vector search results /
 * a stack-group member set, write it to panel state, and switch the panel to
 * the "similar" view scope.
 *
 * Listeners are removed automatically when `panelLifecycleAC` is aborted.
 */
export function bindSimilarSearch({
    similarBtn,
    similarPopover,
    similarFindBtn,
    similarDuplicatesBtn,
    similarSameNodeBtn,
    similarSameWorkflowBtn,
    gridContainer,
    state,
    panelLifecycleAC,
    isAiEnabled,
    similarDisabledTitle,
    readActiveAssetId,
    readSelectedAssetIds,
    readPanelValue,
    writePanelValue,
    scopeController,
    closePopovers,
    closePeerPopovers,
    popovers,
    workflowIdInput,
    reloadGrid,
    getDuplicatesAlert,
}: Record<string, any>): void {
    const selectedAssetId = () =>
        String(readActiveAssetId() || readSelectedAssetIds()[0] || "").trim();

    const selectedNumericAssetId = () => {
        const selectedId = Number(selectedAssetId());
        return Number.isFinite(selectedId) && selectedId > 0 ? selectedId : 0;
    };

    const getActiveAssetFromGrid = () => {
        const activeId = selectedAssetId();
        if (!activeId) return null;
        try {
            const getAssets = gridContainer?._mjrGetAssets;
            const list = typeof getAssets === "function" ? getAssets() : [];
            return Array.isArray(list)
                ? list.find((asset) => String(asset?.id || "") === activeId) || null
                : null;
        } catch (err) {
            console.debug?.(err);
            return null;
        }
    };

    const getActiveAsset = async () => {
        const fromGrid = getActiveAssetFromGrid();
        if (fromGrid) return fromGrid;
        const id = selectedNumericAssetId();
        if (!id) return null;
        return fetchActiveAssetMetadata(id);
    };

    const fetchActiveAssetMetadata = async (id = selectedNumericAssetId()) => {
        if (!id) return null;
        try {
            const res = await getAssetMetadata(id, { timeoutMs: 30_000 });
            return res?.ok ? res.data || null : null;
        } catch (err) {
            console.debug?.(err);
            return null;
        }
    };

    const hydrateActiveAssetWhenMissing = async (asset: any, keys: string[]) => {
        if (asset && keys.some((key) => readAssetField(asset, key))) return asset;
        const fresh = await fetchActiveAssetMetadata();
        return fresh || asset;
    };

    const readNodeContextFiles = (detail: Record<string, any> = {}) => {
        const raw = Array.isArray(detail?.files) ? detail.files : [];
        return raw
            .map((file: any) => ({
                filename: String(file?.filename || "").trim(),
                subfolder: String(file?.subfolder || "").trim().replace(/\\/g, "/"),
                type: String(file?.type || "").trim().toLowerCase(),
            }))
            .filter((file: any) => file.filename);
    };

    /**
     * Fallback used when a node has no indexed source-node context (e.g. a
     * Load Image node): resolve the file(s) referenced by the node against the
     * asset index by filename and show exact matches in the "similar" scope.
     */
    const openNodeFilesFallback = async (detail: Record<string, any>, files: any[]) => {
        const matches: any[] = [];
        const seen = new Set<string>();
        for (const file of files.slice(0, 4)) {
            const scope = file.type === "input" ? "input" : "output";
            try {
                const res = await get(
                    buildListURL({
                        q: file.filename,
                        scope,
                        limit: 50,
                        includeTotal: false,
                    }),
                    { timeoutMs: 30_000 },
                );
                if (!res?.ok) continue;
                const rows = Array.isArray(res?.data?.assets)
                    ? res.data.assets
                    : Array.isArray(res?.data)
                      ? res.data
                      : [];
                const wantedName = file.filename.toLowerCase();
                const wantedSuffix = (
                    file.subfolder ? `${file.subfolder}/${file.filename}` : file.filename
                ).toLowerCase();
                for (const row of rows) {
                    const rowName = String(row?.filename || "").toLowerCase();
                    if (rowName !== wantedName) continue;
                    const rowPath = String(row?.filepath || row?.path || "")
                        .replace(/\\/g, "/")
                        .toLowerCase();
                    if (file.subfolder && rowPath && !rowPath.endsWith(wantedSuffix)) continue;
                    const key = String(row?.id || rowPath || rowName);
                    if (seen.has(key)) continue;
                    seen.add(key);
                    matches.push(row);
                }
            } catch (err) {
                console.debug?.(err);
            }
        }
        if (!matches.length) {
            comfyToast(
                t(
                    "nodeContext.fileNotIndexed",
                    "The node file is not indexed yet. Run a scan first.",
                ),
                "info",
                3200,
            );
            return;
        }
        const nodeLabel = String(
            detail?.title ||
                detail?.sourceNodeType ||
                detail?.source_node_type ||
                files[0]?.filename ||
                "",
        ).trim();
        writePanelValue("similarResults", matches);
        writePanelValue(
            "similarSourceAssetId",
            `node-file:${files[0]?.filename || ""}`,
        );
        writePanelValue(
            "similarTitle",
            t("nodeContext.fileResultsTitle", "Node {node} file ({n} assets)", {
                node: nodeLabel || files[0]?.filename || "",
                n: matches.length,
            }),
        );
        await scopeController?.setScope?.("similar");
    };

    const openNodeContext = async (detail: Record<string, any> = {}) => {
        const sourceNodeId = String(detail?.sourceNodeId || detail?.source_node_id || "").trim();
        const files = readNodeContextFiles(detail);
        if (!sourceNodeId && !files.length) return;
        try {
            closePopovers?.();
        } catch (err) {
            console.debug?.(err);
        }
        if (!sourceNodeId) {
            await openNodeFilesFallback(detail, files);
            return;
        }
        try {
            const res = await get(
                buildNodeContextMembersURL(sourceNodeId, {
                    jobId: detail?.jobId || detail?.job_id || "",
                    latest: detail?.latest !== false,
                    limit: 500,
                }),
                { timeoutMs: 30_000 },
            );
            if (!res?.ok) {
                if (isNodeContextRouteMissing(res)) {
                    if (files.length) {
                        await openNodeFilesFallback(detail, files);
                        return;
                    }
                    const root = ((window as any).MajoorAssetsManager ||= {});
                    if (!root.nodeContextRouteMissingToastShown) {
                        root.nodeContextRouteMissingToastShown = true;
                        comfyToast(
                            t(
                                "nodeContext.routeMissing",
                                "Node context backend route is not loaded yet. Restart ComfyUI after updating Majoor Assets Manager.",
                            ),
                            "warn",
                            7000,
                        );
                    }
                    return;
                }
                if (files.length) {
                    await openNodeFilesFallback(detail, files);
                    return;
                }
                comfyToast(
                    String(res?.error || t("nodeContext.loadFailed", "Failed to load node assets")),
                    "error",
                    3000,
                );
                return;
            }
            const list = Array.isArray(res?.data) ? res.data : [];
            if (!list.length) {
                if (files.length) {
                    await openNodeFilesFallback(detail, files);
                    return;
                }
                comfyToast(
                    t("nodeContext.noAssets", "No indexed assets found for this node yet."),
                    "info",
                    2600,
                );
                return;
            }
            const nodeLabel = String(
                detail?.title || detail?.sourceNodeType || detail?.source_node_type || sourceNodeId,
            ).trim();
            writePanelValue("similarResults", list);
            writePanelValue("similarSourceAssetId", `node:${sourceNodeId}`);
            writePanelValue(
                "similarTitle",
                t("nodeContext.resultsTitle", "Node {node} outputs ({n} assets)", {
                    node: nodeLabel || sourceNodeId,
                    n: list.length,
                }),
            );
            await scopeController?.setScope?.("similar");
        } catch (err) {
            console.debug?.(err);
            comfyToast(t("nodeContext.loadFailed", "Failed to load node assets"), "error", 3000);
        }
    };

    const runFindSimilar = async () => {
        if (!isAiEnabled()) {
            comfyToast(similarDisabledTitle, "info", 2200);
            return;
        }

        try {
            closePopovers();
        } catch (err) {
            console.debug?.(err);
        }

        const selectedId = selectedNumericAssetId();
        if (!selectedId) {
            comfyToast(
                t(
                    "search.selectAssetForSimilar",
                    "Select an asset first to find similar images/videos.",
                ),
                "info",
                2500,
            );
            return;
        }

        const prevTitle = similarBtn.title;
        similarBtn.disabled = true;
        similarBtn.title = t("search.findingSimilar", "Finding similar assets...");
        try {
            const res = await vectorFindSimilar(selectedId, {
                topK: 100,
                scope: state.scope || "output",
                customRootId: state.customRootId || "",
            });
            if (!res?.ok) {
                comfyToast(
                    String(
                        res?.error ||
                            t("search.findSimilarFailed", "Failed to find similar assets"),
                    ),
                    "error",
                    3000,
                );
                return;
            }
            const list = Array.isArray(res?.data) ? res.data : [];
            writePanelValue("similarResults", list);
            writePanelValue("similarSourceAssetId", String(selectedId));
            writePanelValue(
                "similarTitle",
                t("search.similarResults", "Similar to asset #{id} ({n} results)", {
                    id: selectedId,
                    n: list.length,
                }),
            );
            await scopeController?.setScope?.("similar");
        } catch (err) {
            console.debug?.(err);
            comfyToast(
                t("search.findSimilarFailed", "Failed to find similar assets"),
                "error",
                3000,
            );
        } finally {
            similarBtn.disabled = false;
            similarBtn.title = prevTitle;
        }
    };

    const runFindDuplicates = async () => {
        try {
            closePopovers?.();
        } catch (err) {
            console.debug?.(err);
        }
        const activeAsset = await getActiveAsset();
        const activeId = String(activeAsset?.id || selectedAssetId() || "").trim();
        const members = Array.isArray(activeAsset?._mjrDupMembers)
            ? activeAsset._mjrDupMembers
            : [];
        if (members.length >= 2) {
            writePanelValue("similarResults", members);
            writePanelValue("similarSourceAssetId", activeId ? `duplicates:${activeId}` : "duplicates");
            writePanelValue(
                "similarTitle",
                t("search.duplicateResults", "Duplicates ({n} assets)", { n: members.length }),
            );
            await scopeController?.setScope?.("similar");
            return;
        }

        const alert = getDuplicatesAlert?.() || {};
        const group = alert?.firstGroup;
        if (group && Array.isArray(group.assets) && group.assets.length >= 2) {
            writePanelValue("similarResults", group.assets);
            writePanelValue("similarSourceAssetId", "duplicates");
            writePanelValue(
                "similarTitle",
                t("search.duplicateResults", "Duplicates ({n} assets)", { n: group.assets.length }),
            );
            await scopeController?.setScope?.("similar");
            return;
        }

        comfyToast(
            t(
                "search.noKnownDuplicates",
                "No duplicate group is available yet. Run duplicate analysis from the duplicate alert first.",
            ),
            "info",
            3200,
        );
    };

    const runSameSaveNode = async () => {
        const asset = await hydrateActiveAssetWhenMissing(await getActiveAsset(), [
            "source_node_id",
            "source_node_type",
        ]);
        const sourceNodeId = readAssetField(asset, "source_node_id");
        if (!sourceNodeId) {
            comfyToast(
                t("search.noSourceNode", "Selected asset has no persisted source node id."),
                "info",
                2600,
            );
            return;
        }
        await openNodeContext({
            sourceNodeId,
            sourceNodeType: readAssetField(asset, "source_node_type"),
            jobId: readAssetField(asset, "job_id"),
            title: readAssetField(asset, "source_node_type") || sourceNodeId,
        });
    };

    const runSameWorkflow = async () => {
        try {
            closePopovers?.();
        } catch (err) {
            console.debug?.(err);
        }
        const asset = await hydrateActiveAssetWhenMissing(await getActiveAsset(), ["workflow_id"]);
        const workflowId = readAssetField(asset, "workflow_id");
        if (!workflowId) {
            comfyToast(
                t("search.noWorkflowId", "Selected asset has no persisted workflow id."),
                "info",
                2600,
            );
            return;
        }
        writePanelValue("workflowId", workflowId);
        try {
            if (workflowIdInput) workflowIdInput.value = workflowId;
        } catch (err) {
            console.debug?.(err);
        }
        try {
            await scopeController?.setScope?.(state.scope || "output");
        } catch (err) {
            console.debug?.(err);
        }
        await reloadGrid?.();
    };

    // -- Similar menu button + actions --------------------------------------

    similarBtn?.addEventListener(
        "click",
        (e: any) => {
            e.stopPropagation();
            try {
                closePeerPopovers?.();
            } catch (err) {
                console.debug?.(err);
            }
            popovers?.toggle?.(similarPopover, similarBtn);
        },
        { signal: panelLifecycleAC?.signal },
    );

    const bindAction = (button: any, handler: any) => {
        button?.addEventListener(
            "click",
            (event: any) => {
                event.stopPropagation();
                void handler();
            },
            { signal: panelLifecycleAC?.signal },
        );
    };

    bindAction(similarFindBtn, runFindSimilar);
    bindAction(similarDuplicatesBtn, runFindDuplicates);
    bindAction(similarSameNodeBtn, runSameSaveNode);
    bindAction(similarSameWorkflowBtn, runSameWorkflow);

    // -- Stack-group open event ---------------------------------------------

    gridContainer?.addEventListener(
        EVENTS.OPEN_STACK_GROUP,
        async (event: any) => {
            try {
                const detail = event?.detail || {};
                const rawList = Array.isArray(detail?.members) ? detail.members : [];
                const fallbackTitle = `Generation group (${rawList.length} assets)`;
                const limited = limitStackGroupMembers(
                    rawList,
                    String(detail?.title || "").trim() || fallbackTitle,
                );
                if (limited.truncated) {
                    comfyToast(
                        `Large stack truncated to ${STACK_GROUP_RENDER_LIMIT}/${limited.total} assets to keep the grid responsive.`,
                        "warn",
                        5000,
                    );
                }
                writePanelValue("similarResults", limited.list);
                writePanelValue("similarSourceAssetId", buildGroupSourceId(detail));
                writePanelValue("similarTitle", limited.title || fallbackTitle);
                await Promise.resolve();
                await scopeController?.setScope?.("similar");
            } catch (err) {
                console.debug?.(err);
            }
        },
        { signal: panelLifecycleAC?.signal },
    );

    window.addEventListener(
        EVENTS.OPEN_NODE_CONTEXT,
        (event) => {
            try {
                const root = (window as any).MajoorAssetsManager;
                if (root?.pendingNodeContext) root.pendingNodeContext = null;
            } catch (err) {
                console.debug?.(err);
            }
            void openNodeContext((event as any)?.detail || {});
        },
        { signal: panelLifecycleAC?.signal },
    );

    try {
        const pending = (window as any).MajoorAssetsManager?.pendingNodeContext || null;
        if (pending) {
            (window as any).MajoorAssetsManager.pendingNodeContext = null;
            setTimeout(() => {
                void openNodeContext(pending);
            }, 0);
        }
    } catch (err) {
        console.debug?.(err);
    }
}
