import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";

const EXTENSION = "ethanfel.prompt_builder.accumulator_preview";
const NODE_NAME = "SxCPAccumulatorPreview";
const STYLE_ID = "sxcp-accumulator-preview-styles";
const DEBUG_STORAGE_KEY = "sxcpAccumulatorPreviewDebug";

const BASE_CELL_W = 170;
const MIN_CELL_W = 90;
const MAX_CELL_W = 520;
const BASE_GRID_H = 360;
const MIN_GRID_H = 240;
const MAX_GRID_H = 900;
const GAP = 6;
const PAD = 4;
const TOOLBAR_H = 28;
const CAPTION_H = 20;
const EMPTY_GRID_H = 84;
const MIN_W = 560;
const MARGIN = 10;

function widget(node, name) {
    return node.widgets?.find((w) => w.name === name);
}

function hideWidget(w) {
    if (!w) return;
    w.hidden = true;
    w.computeSize = () => [0, -4];
}

function isAccumulatorPreviewNode(node) {
    return node?.comfyClass === NODE_NAME || node?.type === NODE_NAME;
}

function getNodeById(id) {
    return app.graph?.getNodeById?.(Number(id)) || app.graph?._nodes_by_id?.[id] || app.graph?._nodes_by_id?.[Number(id)];
}

function debugEnabled() {
    try {
        return window.SXCP_ACCUMULATOR_PREVIEW_DEBUG === true || localStorage.getItem(DEBUG_STORAGE_KEY) === "1";
    } catch (_err) {
        return window.SXCP_ACCUMULATOR_PREVIEW_DEBUG === true;
    }
}

function debugLog(...args) {
    if (debugEnabled()) console.log(`[${EXTENSION}]`, ...args);
}

function debugWarn(...args) {
    console.warn(`[${EXTENSION}]`, ...args);
}

function nodeSummaries() {
    return (app.graph?._nodes || [])
        .filter(isAccumulatorPreviewNode)
        .map((node) => ({
            id: node.id,
            store_key: storeKey(node),
            widget_store_key: widgetStoreKey(node),
            resolved_store_key: node._sxapResolvedStoreKey || "",
            entries: (node._sxapEntries || []).map((entry, index) => ({
                index: entry.index,
                id: entry.id,
                preview_key: entry.preview_key,
                has_image: entry.has_image,
                has_preview_image: !!entry.preview_image,
                image_ref: imageParamsForEntry(node, entry, index),
            })),
            images: node._sxapImages || [],
            status: node._sxapStatus || "",
        }));
}

function installDebugHelpers() {
    if (window.sxcpAccumulatorPreviewDebug) return;
    window.sxcpAccumulatorPreviewDebug = {
        enable() {
            localStorage.setItem(DEBUG_STORAGE_KEY, "1");
            window.SXCP_ACCUMULATOR_PREVIEW_DEBUG = true;
            console.log(`[${EXTENSION}] debug enabled`);
        },
        disable() {
            localStorage.removeItem(DEBUG_STORAGE_KEY);
            window.SXCP_ACCUMULATOR_PREVIEW_DEBUG = false;
            console.log(`[${EXTENSION}] debug disabled`);
        },
        dump() {
            const data = nodeSummaries();
            console.log(`[${EXTENSION}] dump`, data);
            return data;
        },
    };
}

function asArray(value) {
    if (!value) return [];
    return Array.isArray(value) ? value : [value];
}

function outputStatus(output) {
    const status = output?.status;
    if (Array.isArray(status)) return status[0] || "";
    return status || "";
}

function outputStoreKey(output) {
    const key = output?.store_key;
    if (Array.isArray(key)) return key[0] || "";
    return key || "";
}

function outputEntries(output) {
    const entries = output?.entries;
    if (!entries) return [];
    if (Array.isArray(entries) && entries.length === 1 && Array.isArray(entries[0])) return entries[0];
    return asArray(entries);
}

function outputImages(output) {
    const images = output?.images;
    if (!images) return [];
    if (Array.isArray(images) && images.length === 1 && Array.isArray(images[0])) return images[0];
    return asArray(images).filter(Boolean);
}

function storeKey(node) {
    return String(node._sxapResolvedStoreKey || widget(node, "store_key")?.value || "").trim();
}

function widgetStoreKey(node) {
    return String(widget(node, "store_key")?.value || "").trim();
}

function previewLimit(node) {
    const value = Number(widget(node, "preview_limit")?.value ?? 64);
    return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 64;
}

function clamp(value, min, max) {
    return Math.min(max, Math.max(min, value));
}

function viewMode(node) {
    return widget(node, "view_mode")?.value === "carousel" ? "carousel" : "grid";
}

function zoomLevel(node) {
    const value = Number(widget(node, "zoom_level")?.value ?? 1);
    return Number.isFinite(value) ? clamp(value, 0.5, 3.0) : 1;
}

function carouselPosition(node, count) {
    if (count <= 0) return 1;
    const value = Number(widget(node, "carousel_index")?.value ?? node._sxapCarouselIndex ?? 1);
    return clamp(Number.isFinite(value) ? Math.round(value) : 1, 1, count);
}

function setCarouselPosition(node, position) {
    const count = imageEntries(node).length;
    const next = count > 0 ? clamp(Math.round(Number(position) || 1), 1, count) : 1;
    node._sxapCarouselIndex = next;
    setWidgetValue(node, "carousel_index", next);
    renderGrid(node);
}

function actionPayload(node, values = {}) {
    return {preview_limit: previewLimit(node), ...values};
}

function imageEntries(node) {
    const entries = node._sxapEntries || [];
    return entries.filter((entry) => entry?.has_image).slice(0, previewLimit(node));
}

function entryKey(entry) {
    const key = String(entry?.preview_key || "").trim();
    if (key) return `key:${key}`;
    const id = String(entry?.id || "").trim();
    if (id) return `id:${id}`;
    return `index:${entry?.index ?? ""}`;
}

function buildImageMap(entries, images, previous = new Map()) {
    const next = new Map(previous);
    const imageEntries = asArray(entries).filter((entry) => entry?.has_image);
    imageEntries.forEach((entry) => {
        if (entry.preview_image) next.set(entryKey(entry), entry.preview_image);
    });
    asArray(images).filter(Boolean).forEach((image, index) => {
        const directKey = String(image?.preview_key || "").trim();
        if (directKey) {
            next.set(`key:${directKey}`, image);
            return;
        }
        const entryId = String(image?.entry_id || "").trim();
        if (entryId) {
            next.set(`id:${entryId}`, image);
            return;
        }
        const entry = imageEntries[index];
        if (entry) next.set(entryKey(entry), image);
    });
    return next;
}

function imageParamsForEntry(node, entry, fallbackIndex) {
    if (entry?.preview_image) return entry.preview_image;
    const keyed = node._sxapImageByKey?.get(entryKey(entry));
    if (keyed) return keyed;
    return (node._sxapImages || [])[fallbackIndex];
}

function imageUrl(params) {
    const query = new URLSearchParams(params).toString();
    const preview = app.getPreviewFormatParam?.() || "";
    const rand = app.getRandParam?.() || `&rand=${Math.random()}`;
    return api.apiURL(`/view?${query}${preview}${rand}`);
}

function entryLabel(entry) {
    const parts = [`#${entry?.index ?? "?"}`];
    if (entry?.id) parts.push(entry.id);
    if (Array.isArray(entry?.shape) && entry.shape.length >= 2) parts.push(`${entry.shape[1]}x${entry.shape[0]}`);
    if (entry?.has_metadata) parts.push("metadata");
    return parts.join(" ");
}

function entryTitle(entry) {
    const value = entry?.value ? `\n${entry.value}` : "";
    return `${entryLabel(entry)}${value}`;
}

async function postJson(path, payload) {
    debugLog("POST", path, payload);
    const response = await api.fetchApi(path, {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify(payload),
    });
    const data = await response.json();
    debugLog("POST response", path, data);
    if (!response.ok) throw new Error(data?.error || response.statusText);
    return data;
}

function workflowFromRetakeData(data) {
    const workflow = data?.workflow;
    if (!workflow) throw new Error("No workflow metadata found on this accumulator entry.");
    if (typeof workflow === "string") return JSON.parse(workflow);
    return workflow;
}

async function loadWorkflow(workflow) {
    if (typeof app.loadGraphData === "function") {
        await app.loadGraphData(workflow);
        return;
    }
    if (!app.graph?.configure) throw new Error("This ComfyUI frontend cannot load workflow data.");
    app.graph.clear?.();
    app.graph.configure(workflow);
    app.graph.setDirtyCanvas?.(true, true);
}

function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    const css = `
    .sxap-wrap { display:flex; flex-direction:column; gap:4px; box-sizing:border-box; height:100%; min-height:0; }
    .sxap-grid { display:flex; flex-wrap:wrap; gap:${GAP}px; align-content:flex-start; overflow-y:auto;
                 padding:${PAD}px; background:rgba(0,0,0,0.15); border-radius:4px;
                 flex:1 1 auto; min-height:0; box-sizing:border-box; }
    .sxap-grid.sxap-carousel { flex-wrap:nowrap; align-items:center; justify-content:center; overflow:hidden; }
    .sxap-grid.sxap-dragover { outline:2px dashed #6cf; outline-offset:-2px; }
    .sxap-empty { width:100%; padding:12px; text-align:center; font-size:12px; opacity:0.65; box-sizing:border-box; }
    .sxap-cell { position:relative; width:var(--sxap-cell-w, 170px); border:2px solid transparent;
                 border-radius:4px; overflow:hidden; background:#222; box-sizing:border-box;
                 transition:border-color .1s, background .1s; }
    .sxap-cell:hover { border-color:#555; }
    .sxap-cell.sxap-selected { border-color:#6cf; }
    .sxap-cell.sxap-drop { border-color:#fc6; border-style:dashed; }
    .sxap-cell.sxap-carousel-cell { width:100%; height:100%; display:flex; flex-direction:column; }
    .sxap-thumb { width:100%; height:var(--sxap-thumb-h, 170px); object-fit:contain; display:block;
                  cursor:grab; background:#111; }
    .sxap-carousel-cell .sxap-thumb { flex:1 1 auto; min-height:0; }
    .sxap-thumb:active { cursor:grabbing; }
    .sxap-badge { position:absolute; top:2px; left:2px; font-size:10px; background:rgba(0,0,0,0.65);
                  color:#fff; padding:0 4px; border-radius:3px; pointer-events:none; }
    .sxap-meta { position:absolute; top:3px; left:50%; transform:translateX(-50%); font-size:9px;
                 background:rgba(40,160,100,0.9); color:#fff; padding:0 3px; border-radius:3px;
                 pointer-events:none; }
    .sxap-del { position:absolute; top:2px; right:2px; width:18px; height:18px; border:none; border-radius:3px;
                background:rgba(180,30,30,0.88); color:#fff; font-size:11px; line-height:1;
                cursor:pointer; padding:0; }
    .sxap-caption { width:100%; height:${CAPTION_H}px; box-sizing:border-box;
                    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding:2px 4px;
                    background:rgba(0,0,0,0.62); color:#fff; font-size:10px; line-height:16px; }
    .sxap-toolbar { display:flex; align-items:center; gap:6px; flex:0 0 auto; min-height:${TOOLBAR_H - 2}px; }
    .sxap-toolbar button { font-size:11px; padding:2px 8px; cursor:pointer; }
    .sxap-toolbar button:disabled { opacity:0.45; cursor:default; }
    .sxap-carousel-label { font-size:11px; opacity:0.75; min-width:34px; text-align:center; }
    .sxap-status { min-width:0; margin-left:auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
                   font-size:11px; opacity:0.75; }
    `;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
}

function layoutMetrics(node) {
    const width = Math.max(node.size?.[0] || MIN_W, MIN_W);
    const inner = Math.max(MIN_CELL_W, width - 2 * MARGIN - 2 * PAD);
    const zoom = zoomLevel(node);
    if (viewMode(node) === "carousel") {
        const gridH = clamp(Math.round(BASE_GRID_H * zoom), MIN_GRID_H, MAX_GRID_H);
        return {cellW: inner, thumbH: Math.max(120, gridH - CAPTION_H - 2 * PAD), gridH};
    }
    const desiredCellW = clamp(Math.round(BASE_CELL_W * zoom), MIN_CELL_W, MAX_CELL_W);
    const cellW = Math.min(desiredCellW, inner);
    const thumbH = Math.round(cellW * 1.1);
    const gridH = clamp(Math.round(BASE_GRID_H * Math.max(0.75, Math.min(zoom, 1.75))), MIN_GRID_H, MAX_GRID_H);
    return {cellW, thumbH, gridH};
}

function recomputeSize(node) {
    const {cellW, thumbH, gridH} = layoutMetrics(node);
    node._sxapCellW = cellW;
    node._sxapThumbH = thumbH;
    node._sxapWidgetH = 2 * MARGIN + TOOLBAR_H + 6 + Math.max(gridH, EMPTY_GRID_H);
}

function syncWidgetWidth(node) {
    if (node._sxapWidget) node._sxapWidget.width = node.size?.[0] || MIN_W;
}

function resizeToContent(node, force = false) {
    recomputeSize(node);
    const targetH = node.computeSize?.()[1] || node._sxapWidgetH || 140;
    const width = node.size?.[0] || MIN_W;
    const currentH = node.size?.[1] || 0;
    const nextH = force || currentH <= 0 ? targetH : Math.max(currentH, targetH);
    node._sxapAutoHeight = nextH;
    if (Math.abs(currentH - nextH) > 1) node.setSize?.([width, nextH]);
    syncWidgetWidth(node);
    node.setDirtyCanvas?.(true, true);
}

function suppressBuiltinPreview(node) {
    try {
        Object.defineProperty(node, "imgs", {
            configurable: true,
            get() { return undefined; },
            set() {},
        });
    } catch (_err) {
        // Best effort: if another extension made it non-configurable, the grid still works.
    }
}

function setWidgetValue(node, name, value) {
    const w = widget(node, name);
    if (!w) return;
    w.value = value;
    w.callback?.(value, app.canvas, node);
}

function setStatus(node, status) {
    node._sxapStatus = status || "";
    if (node._sxapStatusEl) node._sxapStatusEl.textContent = status || "";
    node.setDirtyCanvas?.(true, true);
}

function clearDropState(node) {
    node._sxapGridEl?.querySelectorAll?.(".sxap-drop").forEach((cell) => cell.classList.remove("sxap-drop"));
    node._sxapGridEl?.classList.remove("sxap-dragover");
}

function markSelected(node, selectedIndex) {
    node._sxapSelectedIndex = selectedIndex;
    node._sxapGridEl?.querySelectorAll?.(".sxap-cell").forEach((cell) => {
        cell.classList.toggle("sxap-selected", Number(cell.dataset.index || 0) === selectedIndex);
    });
}

function renderCell(node, entry, imageParams, displayIndex, options = {}) {
    const cell = document.createElement("div");
    cell.className = "sxap-cell"
        + (options.carousel ? " sxap-carousel-cell" : "")
        + (entry.index === node._sxapSelectedIndex ? " sxap-selected" : "");
    cell.dataset.index = String(entry.index || "");
    cell.title = entryTitle(entry);
    const cellW = node._sxapCellW || MIN_CELL_W;
    cell.style.setProperty("--sxap-cell-w", `${cellW}px`);
    cell.style.setProperty("--sxap-thumb-h", `${node._sxapThumbH || Math.round(cellW * 1.1)}px`);

    cell.ondragover = (event) => {
        if (!node._sxapDragEntry) return;
        event.preventDefault();
        event.stopPropagation();
        if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
        cell.classList.add("sxap-drop");
    };
    cell.ondragleave = () => cell.classList.remove("sxap-drop");
    cell.ondrop = async (event) => {
        if (!node._sxapDragEntry) return;
        event.preventDefault();
        event.stopPropagation();
        cell.classList.remove("sxap-drop");
        const source = node._sxapDragEntry;
        node._sxapDragEntry = null;
        if (source.index === entry.index) return;
        debugLog("drop", {
            source_index: source.index,
            source_key: entryKey(source),
            target_index: entry.index,
            target_key: entryKey(entry),
        });
        await moveEntryToIndex(node, source, entry.index);
    };

    const thumb = document.createElement("img");
    thumb.className = "sxap-thumb";
    if (imageParams) {
        const url = imageUrl(imageParams);
        thumb.src = url;
        thumb.onerror = () => debugWarn("image load failed", {
            entry: {index: entry.index, id: entry.id, preview_key: entry.preview_key},
            imageParams,
            url,
        });
    } else {
        debugWarn("missing image params for entry", {
            entry: {index: entry.index, id: entry.id, preview_key: entry.preview_key},
            displayIndex,
        });
    }
    thumb.draggable = true;
    thumb.onclick = () => markSelected(node, entry.index);
    thumb.oncontextmenu = async (event) => {
        event.preventDefault();
        event.stopPropagation();
        markSelected(node, entry.index);
        await retakeEntry(node, entry);
    };
    thumb.ondragstart = (event) => {
        node._sxapDragEntry = entry;
        debugLog("dragstart", {index: entry.index, key: entryKey(entry), id: entry.id});
        if (event.dataTransfer) {
            event.dataTransfer.effectAllowed = "move";
            event.dataTransfer.setData("text/plain", String(entry.id || entry.index || ""));
        }
    };
    thumb.ondragend = () => {
        node._sxapDragEntry = null;
        clearDropState(node);
    };
    cell.appendChild(thumb);

    const badge = document.createElement("div");
    badge.className = "sxap-badge";
    badge.textContent = String(entry.index ?? displayIndex + 1);
    cell.appendChild(badge);

    if (entry.has_metadata) {
        const meta = document.createElement("div");
        meta.className = "sxap-meta";
        meta.textContent = "M";
        meta.title = "has metadata; right-click image to retake";
        cell.appendChild(meta);
    }

    const del = document.createElement("button");
    del.className = "sxap-del";
    del.textContent = "x";
    del.title = "Delete";
    del.onclick = async (event) => {
        event.stopPropagation();
        await deleteEntry(node, entry);
    };
    cell.appendChild(del);

    const caption = document.createElement("div");
    caption.className = "sxap-caption";
    caption.textContent = entry.id || entry.value || entryLabel(entry);
    cell.appendChild(caption);

    return cell;
}

function updateCarouselControls(node, count) {
    const carousel = viewMode(node) === "carousel";
    const position = carouselPosition(node, count);
    if (node._sxapPrevButton) {
        node._sxapPrevButton.style.display = carousel ? "" : "none";
        node._sxapPrevButton.disabled = !carousel || count <= 1;
    }
    if (node._sxapNextButton) {
        node._sxapNextButton.style.display = carousel ? "" : "none";
        node._sxapNextButton.disabled = !carousel || count <= 1;
    }
    if (node._sxapCarouselLabel) {
        node._sxapCarouselLabel.style.display = carousel ? "" : "none";
        node._sxapCarouselLabel.textContent = count ? `${position}/${count}` : "0/0";
    }
}

function renderGrid(node) {
    const grid = node._sxapGridEl;
    if (!grid) return;
    const entries = imageEntries(node);
    const carousel = viewMode(node) === "carousel";
    grid.replaceChildren();
    grid.classList.toggle("sxap-carousel", carousel);

    if (!entries.length) {
        const empty = document.createElement("div");
        empty.className = "sxap-empty";
        empty.textContent = storeKey(node) ? "No accumulator images." : "Run once or set an explicit store_key.";
        grid.appendChild(empty);
    } else if (carousel) {
        const position = carouselPosition(node, entries.length);
        const entry = entries[position - 1];
        if (Number(widget(node, "carousel_index")?.value || 1) !== position) {
            setWidgetValue(node, "carousel_index", position);
        }
        markSelected(node, entry.index);
        grid.appendChild(renderCell(node, entry, imageParamsForEntry(node, entry, position - 1), position - 1, {carousel: true}));
    } else {
        debugLog("renderGrid", entries.map((entry, index) => ({
            index: entry.index,
            key: entryKey(entry),
            has_preview_image: !!entry.preview_image,
            has_image_params: !!imageParamsForEntry(node, entry, index),
        })));
        entries.forEach((entry, index) => {
            grid.appendChild(renderCell(node, entry, imageParamsForEntry(node, entry, index), index));
        });
    }

    const total = (node._sxapEntries || []).filter((entry) => entry?.has_image).length;
    const count = entries.length;
    updateCarouselControls(node, count);
    if (node._sxapStatusEl) {
        const prefix = carousel && count
            ? `image ${carouselPosition(node, count)}/${count}`
            : total > count ? `${count}/${total} shown` : `${total} image${total === 1 ? "" : "s"}`;
        node._sxapStatusEl.textContent = node._sxapStatus ? `${prefix}; ${node._sxapStatus}` : prefix;
        node._sxapStatusEl.title = node._sxapStatusEl.textContent;
    }

    if (count !== node._sxapLastCount) {
        node._sxapLastCount = count;
        requestAnimationFrame(() => resizeToContent(node));
    } else {
        recomputeSize(node);
        syncWidgetWidth(node);
        node.setDirtyCanvas?.(true, true);
    }
}

function applyData(node, data, status = "") {
    if (Object.prototype.hasOwnProperty.call(data || {}, "store_key")) {
        node._sxapResolvedStoreKey = data.store_key || "";
    }
    if (Object.prototype.hasOwnProperty.call(data || {}, "entries")) {
        node._sxapEntries = asArray(data.entries);
    }
    if (Object.prototype.hasOwnProperty.call(data || {}, "images")) {
        node._sxapImages = asArray(data.images).filter(Boolean);
    }
    node._sxapImageByKey = buildImageMap(node._sxapEntries, node._sxapImages, node._sxapImageByKey);
    setStatus(node, status || data?.status || "");
    renderGrid(node);
}

async function refreshEntries(node) {
    const key = storeKey(node);
    if (!key) {
        setStatus(node, "no store_key yet");
        renderGrid(node);
        return;
    }
    try {
        const data = await postJson("/sxcp/accumulator/list", actionPayload(node, {store_key: key}));
        applyData(node, data, data.status || "");
    } catch (err) {
        console.error(`[${EXTENSION}] refresh failed`, err);
        alert(`Refresh failed: ${err}`);
    }
}

async function deleteEntry(node, entry) {
    const key = storeKey(node);
    if (!key) {
        alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
        return;
    }
    if (!entry) return;
    try {
        const data = await postJson("/sxcp/accumulator/delete", actionPayload(node, {
            store_key: key,
            preview_key: entry.preview_key || "",
            entry_id: entry.id || "",
            index: entry.preview_key || entry.id ? 0 : entry.index,
            clear: false,
        }));
        applyData(node, data, `${data.status || ""}; deleted=${data.removed || 0}`);
    } catch (err) {
        console.error(`[${EXTENSION}] delete failed`, err);
        alert(`Delete failed: ${err}`);
    }
}

async function moveEntryToIndex(node, entry, targetIndex) {
    const key = storeKey(node);
    if (!key) {
        alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
        return;
    }
    if (!entry) return;
    try {
        debugLog("move request", {
            entry: {index: entry.index, id: entry.id, preview_key: entry.preview_key},
            targetIndex,
        });
        const data = await postJson("/sxcp/accumulator/move", actionPayload(node, {
            store_key: key,
            preview_key: entry.preview_key || "",
            entry_id: entry.id || "",
            index: entry.preview_key || entry.id ? 0 : entry.index,
            target_index: targetIndex,
        }));
        debugLog("move response summary", {
            moved: data.moved,
            from_index: data.from_index,
            to_index: data.to_index,
            entries: asArray(data.entries).map((item) => ({
                index: item.index,
                id: item.id,
                preview_key: item.preview_key,
                has_preview_image: !!item.preview_image,
            })),
            images: asArray(data.images).map((item) => ({
                filename: item.filename,
                preview_key: item.preview_key,
                entry_id: item.entry_id,
            })),
        });
        applyData(node, data, `${data.status || ""}; moved=${data.moved ? "yes" : "no"}`);
    } catch (err) {
        console.error(`[${EXTENSION}] drag move failed`, err);
        alert(`Move failed: ${err}`);
    }
}

async function clearStore(node) {
    const key = storeKey(node);
    if (!key) {
        alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
        return;
    }
    if (!confirm(`Clear all entries from accumulator "${key}"?`)) return;
    try {
        const data = await postJson("/sxcp/accumulator/delete", actionPayload(node, {store_key: key, clear: true}));
        applyData(node, data, `${data.status || ""}; cleared=${data.removed || 0}`);
    } catch (err) {
        console.error(`[${EXTENSION}] clear failed`, err);
        alert(`Clear failed: ${err}`);
    }
}

async function saveBatch(node) {
    const key = storeKey(node);
    if (!key) {
        alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
        return;
    }
    try {
        const data = await postJson("/sxcp/accumulator/save", actionPayload(node, {
            store_key: key,
            save_path: widget(node, "save_path")?.value || "sxcp_accumulator",
            filename_prefix: widget(node, "filename_prefix")?.value || "sxcp_accum",
            clear_after_save: !!widget(node, "clear_after_save")?.value,
        }));
        applyData(node, data, data.status || `saved=${data.saved || 0}`);
    } catch (err) {
        console.error(`[${EXTENSION}] direct save failed`, err);
        alert(`Save failed: ${err}`);
    }
}

async function retakeEntry(node, entry) {
    const key = storeKey(node);
    if (!key) {
        alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
        return;
    }
    if (!entry) return;
    if (!entry.has_metadata) {
        alert("This accumulator entry has no metadata to retake from.");
        return;
    }
    const label = entry.id || `#${entry.index}`;
    if (!confirm(`Retake ${label}? This will replace the current workflow with the workflow metadata saved on that entry.`)) return;
    try {
        const data = await postJson("/sxcp/accumulator/retake", {
            store_key: key,
            preview_key: entry.preview_key || "",
            entry_id: entry.id || "",
            index: entry.preview_key || entry.id ? 0 : entry.index,
        });
        const workflow = workflowFromRetakeData(data);
        await loadWorkflow(workflow);
    } catch (err) {
        console.error(`[${EXTENSION}] retake failed`, err);
        alert(`Retake failed: ${err}`);
    }
}

function hideInternalWidgets(node) {
    for (const name of [
        "delete_action",
        "delete_entry_id",
        "delete_index",
        "save_batch",
        "finished",
        "selected_entry",
        "entry_action",
        "accumulator_status",
    ]) {
        hideWidget(widget(node, name));
    }
    for (const legacyButton of [
        "Move Selected Top",
        "Move Selected Up",
        "Move Selected Down",
        "Move Selected Bottom",
        "Delete Selected Entry",
        "Apply Entry Action",
        "Clear Accumulator",
        "Refresh Entry List",
    ]) {
        hideWidget(widget(node, legacyButton));
    }
}

function refreshLayout(node) {
    recomputeSize(node);
    renderGrid(node);
    resizeToContent(node, true);
}

function wrapWidgetCallback(node, name) {
    const w = widget(node, name);
    if (!w || w._sxapWrapped) return;
    const original = w.callback;
    w.callback = function () {
        const result = original?.apply(this, arguments);
        requestAnimationFrame(() => refreshLayout(node));
        return result;
    };
    w._sxapWrapped = true;
}

function installWidgetRefreshHandlers(node) {
    wrapWidgetCallback(node, "view_mode");
    wrapWidgetCallback(node, "zoom_level");
    wrapWidgetCallback(node, "carousel_index");
}

function setupNode(node) {
    injectStyles();
    suppressBuiltinPreview(node);
    hideInternalWidgets(node);
    installWidgetRefreshHandlers(node);

    if (!node._sxapGridEl && typeof node.addDOMWidget === "function") {
        const wrap = document.createElement("div");
        wrap.className = "sxap-wrap";

        const grid = document.createElement("div");
        grid.className = "sxap-grid";
        wrap.appendChild(grid);

        const toolbar = document.createElement("div");
        toolbar.className = "sxap-toolbar";

        const save = document.createElement("button");
        save.textContent = "Save";
        save.title = "Save current accumulator batch once";
        save.onclick = () => saveBatch(node);

        const refresh = document.createElement("button");
        refresh.textContent = "Refresh";
        refresh.onclick = () => refreshEntries(node);

        const clear = document.createElement("button");
        clear.textContent = "Clear";
        clear.onclick = () => clearStore(node);

        const prev = document.createElement("button");
        prev.textContent = "Prev";
        prev.title = "Previous carousel image";
        prev.onclick = () => {
            const count = imageEntries(node).length;
            setCarouselPosition(node, carouselPosition(node, count) - 1);
        };

        const next = document.createElement("button");
        next.textContent = "Next";
        next.title = "Next carousel image";
        next.onclick = () => {
            const count = imageEntries(node).length;
            setCarouselPosition(node, carouselPosition(node, count) + 1);
        };

        const carouselLabel = document.createElement("span");
        carouselLabel.className = "sxap-carousel-label";

        const status = document.createElement("span");
        status.className = "sxap-status";

        toolbar.append(save, refresh, clear, prev, next, carouselLabel, status);
        wrap.appendChild(toolbar);

        node._sxapGridEl = grid;
        node._sxapStatusEl = status;
        node._sxapPrevButton = prev;
        node._sxapNextButton = next;
        node._sxapCarouselLabel = carouselLabel;
        node._sxapEntries = node._sxapEntries || [];
        node._sxapImages = node._sxapImages || [];
        node._sxapImageByKey = node._sxapImageByKey || new Map();
        node._sxapLastCount = -1;
        recomputeSize(node);
        node._sxapWidget = node.addDOMWidget("accumulator_grid", "div", wrap, {
            serialize: false,
            getMinHeight: () => node._sxapWidgetH || 140,
        });
        renderGrid(node);
    }

    const onResize = node.onResize;
    if (!node._sxapResizeWrapped) {
        node.onResize = function () {
            const result = onResize?.apply(this, arguments);
            recomputeSize(node);
            syncWidgetWidth(node);
            return result;
        };
        node._sxapResizeWrapped = true;
    }

    syncWidgetWidth(node);
    if (!node._sxapInitialSizeSet) {
        node._sxapInitialSizeSet = true;
        node.setSize?.([Math.max(node.size?.[0] || 0, MIN_W), node.computeSize?.()[1] || node.size?.[1] || 180]);
    }
    renderGrid(node);
}

app.registerExtension({
    name: EXTENSION,

    async setup() {
        installDebugHelpers();
        api.addEventListener("executed", ({detail}) => {
            const node = getNodeById(detail?.display_node ?? detail?.node);
            if (!isAccumulatorPreviewNode(node)) return;
            const output = detail?.output || {};
            const key = outputStoreKey(output);
            if (key) node._sxapResolvedStoreKey = key;
            applyData(node, {
                store_key: key,
                entries: outputEntries(output),
                images: outputImages(output),
                status: outputStatus(output),
            }, outputStatus(output));
        });
    },

    async beforeRegisterNodeDef(nodeType, nodeData) {
        if (nodeData.name !== NODE_NAME) return;

        const onNodeCreated = nodeType.prototype.onNodeCreated;
        nodeType.prototype.onNodeCreated = function () {
            const result = onNodeCreated?.apply(this, arguments);
            setupNode(this);
            return result;
        };

        const onConfigure = nodeType.prototype.onConfigure;
        nodeType.prototype.onConfigure = function () {
            const result = onConfigure?.apply(this, arguments);
            queueMicrotask(() => {
                setupNode(this);
                refreshEntries(this);
            });
            return result;
        };
    },
});
