/**
 * Host Adapter Layer  -  Majoor Assets Manager
 *
 * This is the SINGLE contracted boundary between Majoor application code
 * (Vue components, Pinia stores, feature modules) and the ComfyUI host
 * environment (DOM, ComfyUI APIs, extension manager, sidebar/panel system).
 *
 * Rules (see ADR 0007):
 *   1. Vue components and Pinia stores MUST import host capabilities from this
 *      module only.  Direct use of `comfyApiBridge.js` from store/component
 *      code is a violation.
 *   2. Only `entry.js` and this file are allowed to import from
 *      `comfyApiBridge.js` directly.
 *   3. Each method exposed here has a stable name and signature.
 *      The underlying ComfyUI API may change; fixes stay inside this file.
 *   4. Every method is null-safe and never throws to callers.
 *
 * Lifecycle
 * ---------
 * Call `hostAdapter.init(app)` once from `entry.js` after receiving the
 * ComfyUI app object.  After init, all methods work correctly.
 * Before init, all methods return safe no-op fallbacks.
 */

import type { MajoorComfyApp, MajoorComfyApi, MajoorExtensionManager } from "../types/comfyui-frontend.js";
import {
    activateBottomPanelTabCompat,
    activateSidebarTabCompat,
    fetchComfyApi,
    getComfyApi,
    getComfyApp,
    getExtensionDialogApi,
    getExtensionManager,
    getExtensionToastApi,
    getSettingValue,
    getSettingsApi,
    registerBottomPanelTabCompat,
    registerCommandCompat,
    registerKeybindingCompat,
    registerSidebarTabCompat,
    setComfyApp,
    setSettingValue,
    waitForComfyApi,
    waitForComfyApp,
} from "./comfyApiBridge.js";
import { findGraphNodeById, getHostRootGraph, walkGraphNodes } from "./graphTraversal.js";

// -- internal state ------------------------------------------------------------

let _app: any = null;
const HOST_QUEUE_PROMPT_BINDING_KEY = Symbol.for("mjr.host.queuePromptBinding");
const HOST_CANVAS_SELECTION_EVENT_TYPES = [
    "selectionchange",
    "selection-change",
    "node-selected",
    "node-deselected",
    "node-selection-change",
];

// -- lifecycle -----------------------------------------------------------------

/**
 * Initialise the adapter with the ComfyUI app object.
 * Must be called once from `entry.js` before any other method.
 *
 * @param {object} app - ComfyUI app object received in the extension setup callback.
 */
export function init(app: any): void {
    _app = setComfyApp(app) || (app as MajoorComfyApp) || null;
}

/**
 * Returns true when the adapter has been initialised with a live app reference.
 */
export function isReady(): boolean {
    return !!(_app || getComfyApp());
}

/**
 * Wait until the host app is available (for code running before `init()`).
 * Resolves with the app object or null on timeout.
 *
 * @param {number} [timeoutMs=5000]
 * @returns {Promise<object | null>}
 */
export async function ready(timeoutMs = 5000): Promise<any> {
    if (_app) return _app;
    try {
        const app = await waitForComfyApp({ timeoutMs });
        if (app) _app = app;
        return _app;
    } catch {
        return null;
    }
}

// -- notifications -------------------------------------------------------------

/**
 * Show a toast notification via ComfyUI extension manager toast.
 * Falls back silently if the host API is unavailable.
 *
 * @param {{ severity?: "info"|"success"|"warn"|"error", summary?: string, detail?: string, life?: number }} opts
 */
export function showToast(opts: { severity?: string; summary?: string; detail?: string; life?: number } | null | undefined): void {
    try {
        const toastApi = getExtensionToastApi(_app || getComfyApp());
        if (toastApi) {
            toastApi.add({
                severity: opts?.severity ?? "info",
                summary: opts?.summary ?? "",
                detail: opts?.detail ?? "",
                life: opts?.life ?? 4000,
            });
        }
    } catch {
        // host not available  -  silent
    }
}

/**
 * Returns the raw ComfyUI toast API when available.
 */
export function getHostToastApi(app: any = null): any {
    try {
        return getExtensionToastApi(app || _app || getComfyApp()) || null;
    } catch {
        return null;
    }
}

// -- dialogs -------------------------------------------------------------------

/**
 * Show a confirmation dialog.
 * Returns a Promise<boolean> (true = confirmed, false = cancelled / unavailable).
 *
 * @param {{ message: string, header?: string }} opts
 * @returns {Promise<boolean>}
 */
export async function confirm(opts: { message: string; header?: string } | null | undefined): Promise<boolean> {
    try {
        const dialogApi = getExtensionDialogApi(_app || getComfyApp());
        if (dialogApi && typeof dialogApi.confirm === "function") {
            const result = await dialogApi.confirm({
                message: opts?.message ?? "",
                header: opts?.header ?? "Confirm",
            });
            return !!result;
        }
    } catch {
        // fall through
    }
    return false;
}

/**
 * Returns the raw ComfyUI dialog API when available.
 */
export function getHostDialogApi(app: any = null): any {
    try {
        return getExtensionDialogApi(app || _app || getComfyApp()) || null;
    } catch {
        return null;
    }
}

// -- settings ------------------------------------------------------------------

/**
 * Read a ComfyUI setting value.
 *
 * @param {string} key
 * @param {*} [defaultValue=null]
 * @returns {*}
 */
export function getSetting(key: string, defaultValue: any = null): any {
    try {
        const value = getSettingValue(_app || getComfyApp(), key);
        return value !== null && value !== undefined ? value : defaultValue;
    } catch {
        return defaultValue;
    }
}

export function getSettingForApp(app: any, key: string, defaultValue: any = null): any {
    try {
        const value = getSettingValue(app || _app || getComfyApp(), key);
        return value !== null && value !== undefined ? value : defaultValue;
    } catch {
        return defaultValue;
    }
}

/**
 * Write a ComfyUI setting value.
 *
 * @param {string} key
 * @param {*} value
 * @returns {boolean}
 */
export function setSetting(key: string, value: any): boolean {
    try {
        return setSettingValue(_app || getComfyApp(), key, value);
    } catch {
        return false;
    }
}

export function setSettingForApp(app: any, key: string, value: any): boolean {
    try {
        return setSettingValue(app || _app || getComfyApp(), key, value);
    } catch {
        return false;
    }
}

/**
 * Get the raw ComfyUI settings API (for registration, not for reading ad-hoc values).
 * Prefer `getSetting()` for reading individual settings.
 *
 * @returns {object | null}
 */
export function getHostSettingsApi(): any {
    try {
        return getSettingsApi(_app || getComfyApp());
    } catch {
        return null;
    }
}

// -- sidebar & panel -----------------------------------------------------------

/**
 * Register a sidebar tab with the ComfyUI extension manager.
 *
 * @param {object} tabDef
 * @returns {boolean} true if registration succeeded
 */
export function registerSidebarTab(tabDef: any): boolean {
    try {
        return registerSidebarTabCompat(_app || getComfyApp(), tabDef);
    } catch {
        return false;
    }
}

export function registerSidebarTabForApp(app: any, tabDef: any): boolean {
    try {
        return registerSidebarTabCompat(app || _app || getComfyApp(), tabDef);
    } catch {
        return false;
    }
}

/**
 * Register a bottom panel tab.
 *
 * @param {object} tabDef
 * @returns {boolean}
 */
export function registerBottomPanelTab(tabDef: any): boolean {
    try {
        return registerBottomPanelTabCompat(_app || getComfyApp(), tabDef);
    } catch {
        return false;
    }
}

export function registerBottomPanelTabForApp(app: any, tabDef: any): boolean {
    try {
        return registerBottomPanelTabCompat(app || _app || getComfyApp(), tabDef);
    } catch {
        return false;
    }
}

/**
 * Programmatically activate a sidebar tab.
 *
 * @param {string} tabId
 * @returns {boolean}
 */
export function activateSidebarTab(tabId: string): boolean {
    try {
        return activateSidebarTabCompat(_app || getComfyApp(), tabId);
    } catch {
        return false;
    }
}

export function activateSidebarTabForApp(app: any, tabId: string): boolean {
    try {
        return activateSidebarTabCompat(app || _app || getComfyApp(), tabId);
    } catch {
        return false;
    }
}

export function activateBottomPanelTabForApp(app: any, tabId: string): boolean {
    try {
        return activateBottomPanelTabCompat(app || _app || getComfyApp(), tabId);
    } catch {
        return false;
    }
}

// -- commands & keybindings ----------------------------------------------------

/**
 * Register a command with the ComfyUI command palette.
 *
 * @param {object} commandDef
 * @returns {boolean}
 */
export function registerCommand(commandDef: any): boolean {
    try {
        return registerCommandCompat(_app || getComfyApp(), commandDef);
    } catch {
        return false;
    }
}

export function registerCommandForApp(app: any, commandDef: any): boolean {
    try {
        return registerCommandCompat(app || _app || getComfyApp(), commandDef);
    } catch {
        return false;
    }
}

/**
 * Register a keybinding.
 *
 * @param {object} keybindingDef
 * @returns {boolean}
 */
export function registerKeybinding(keybindingDef: any): boolean {
    try {
        return registerKeybindingCompat(_app || getComfyApp(), keybindingDef);
    } catch {
        return false;
    }
}

export function registerKeybindingForApp(app: any, keybindingDef: any): boolean {
    try {
        return registerKeybindingCompat(app || _app || getComfyApp(), keybindingDef);
    } catch {
        return false;
    }
}

// -- extension manager (escape hatch) -----------------------------------------

/**
 * Returns the ComfyUI extension manager object.
 *
 * This is an **escape hatch** for capabilities not yet wrapped above.
 * Prefer adding a typed method to this adapter instead of using this.
 *
 * @returns {object | null}
 */
export function getHostExtensionManager(): any {
    try {
        return getExtensionManager(_app || getComfyApp());
    } catch {
        return null;
    }
}

// -- raw app reference (escape hatch) -----------------------------------------

/**
 * Returns the raw ComfyUI app reference.
 *
 * This is an **escape hatch**.  Only use it when you need something that is
 * not yet abstracted by this adapter (and then add it here).
 *
 * @returns {object | null}
 */
export function getRawHostApp(): any {
    return _app || getComfyApp() || null;
}

/**
 * Returns the raw ComfyUI API reference.
 *
 * This is an escape hatch for event subscription and queue APIs that are not
 * wrapped by the adapter yet.
 *
 * @param {object | null} [app=null]
 * @returns {object | null}
 */
export function getRawHostApi(app: any = null): any {
    try {
        return getComfyApi(app || _app || getComfyApp()) || null;
    } catch {
        return null;
    }
}

/**
 * Waits for the raw ComfyUI API reference.
 *
 * @param {object} [options]
 * @returns {Promise<object | null>}
 */
export async function waitForRawHostApi(options: { timeoutMs?: number; warnOnTimeout?: boolean; rejectOnTimeout?: boolean } = {}): Promise<any> {
    try {
        return await waitForComfyApi(options);
    } catch {
        return null;
    }
}

/**
 * Send an HTTP request through ComfyUI's API layer when available.
 * Falls back to the bridge compatibility logic when the host is still warming up.
 *
 * @param {string} url
 * @param {RequestInit | null} [options=null]
 * @param {object | null} [app=null]
 * @returns {Promise<Response>}
 */
export async function fetchHostApi(url: string, options: RequestInit | null = null, app: any = null): Promise<Response> {
    try {
        return await fetchComfyApi(url, options, app || _app || getComfyApp());
    } catch {
        return fetch(url, { credentials: "include", ...(options || {}) });
    }
}

function _safeAddEventListener(target: any, type: string, handler: EventListener): (() => void) | null {
    if (!target || typeof target.addEventListener !== "function") return null;
    try {
        target.addEventListener(type, handler);
    } catch {
        return null;
    }
    return () => {
        try {
            target.removeEventListener?.(type, handler);
        } catch (e) {
            console.debug?.(e);
        }
    };
}

export function observeHostCanvasSelection(
    onSelectionChange: (() => void) | null | undefined,
    options: { app?: any; includePointerFallback?: boolean } = {},
): (() => void) | null {
    if (typeof onSelectionChange !== "function") return null;
    const app = options.app || _app || getComfyApp();
    const canvas = app?.canvas;
    if (!canvas) return null;

    const cleanups: Array<() => void> = [];
    let callbackBound = false;
    const eventTargets = [canvas, app, app?.graph].filter(
        (target, index, all) => target && all.indexOf(target) === index,
    );

    const scheduleSelectionChange = () => {
        try {
            onSelectionChange();
        } catch (e) {
            console.debug?.(e);
        }
    };

    for (const target of eventTargets) {
        for (const type of HOST_CANVAS_SELECTION_EVENT_TYPES) {
            const cleanup = _safeAddEventListener(target, type, scheduleSelectionChange);
            if (cleanup) {
                cleanups.push(cleanup);
                callbackBound = true;
            }
        }
    }

    if (!callbackBound) {
        const hadOwnOnNodeSelected = Object.prototype.hasOwnProperty.call(canvas, "onNodeSelected");
        const hadOwnOnSelectionChange = Object.prototype.hasOwnProperty.call(
            canvas,
            "onSelectionChange",
        );
        const hadOwnOnNodeDeselected = Object.prototype.hasOwnProperty.call(
            canvas,
            "onNodeDeselected",
        );
        const originalOnNodeSelected = canvas.onNodeSelected;
        const originalOnSelectionChange = canvas.onSelectionChange;
        const originalOnNodeDeselected = canvas.onNodeDeselected;

        canvas.onNodeSelected = function (node: any) {
            originalOnNodeSelected?.call(this, node);
            scheduleSelectionChange();
        };
        canvas.onSelectionChange = function (selectedNodes: any) {
            originalOnSelectionChange?.call(this, selectedNodes);
            scheduleSelectionChange();
        };
        canvas.onNodeDeselected = function (node: any) {
            originalOnNodeDeselected?.call(this, node);
            scheduleSelectionChange();
        };

        cleanups.push(() => {
            try {
                if (hadOwnOnNodeSelected) canvas.onNodeSelected = originalOnNodeSelected;
                else delete canvas.onNodeSelected;
                if (hadOwnOnSelectionChange) canvas.onSelectionChange = originalOnSelectionChange;
                else delete canvas.onSelectionChange;
                if (hadOwnOnNodeDeselected) canvas.onNodeDeselected = originalOnNodeDeselected;
                else delete canvas.onNodeDeselected;
            } catch (e) {
                console.debug?.(e);
            }
        });
    }

    if (options.includePointerFallback !== false && canvas.canvas?.addEventListener) {
        const cleanup = _safeAddEventListener(canvas.canvas, "pointerup", scheduleSelectionChange);
        if (cleanup) cleanups.push(cleanup);
    }

    let disposed = false;
    return () => {
        if (disposed) return;
        disposed = true;
        for (const cleanup of cleanups.splice(0).reverse()) {
            cleanup();
        }
    };
}

export function wrapHostQueuePrompt(
    options: {
        api?: any;
        app?: any;
        owner?: any;
        createWrapper?: ((originalQueuePrompt: any, api: any) => any) | null;
    } = {},
): {
    api: any;
    owner: any;
    originalQueuePrompt: any;
    wrappedQueuePrompt: any;
    restore: () => boolean;
} | null {
    const api = options.api || getRawHostApi(options.app || _app || getComfyApp());
    const owner = options.owner || null;
    const createWrapper =
        typeof options.createWrapper === "function" ? options.createWrapper : null;
    if (!api || typeof api.queuePrompt !== "function" || !createWrapper) return null;

    const existingBinding = api.queuePrompt?.[HOST_QUEUE_PROMPT_BINDING_KEY] || null;
    if (existingBinding?.owner === owner) {
        return existingBinding;
    }
    if (existingBinding?.owner && existingBinding.owner !== owner) {
        return null;
    }

    const originalQueuePrompt = api.queuePrompt;
    const wrappedQueuePrompt = createWrapper(originalQueuePrompt, api);
    if (typeof wrappedQueuePrompt !== "function") return null;

    const binding = {
        api,
        owner,
        originalQueuePrompt,
        wrappedQueuePrompt,
        restore: () => {
            try {
                const activeBinding = api.queuePrompt?.[HOST_QUEUE_PROMPT_BINDING_KEY] || null;
                if (activeBinding?.owner !== owner) return false;
                api.queuePrompt = originalQueuePrompt;
                return true;
            } catch (e) {
                console.debug?.(e);
                return false;
            }
        },
    };

    Object.defineProperty(wrappedQueuePrompt, HOST_QUEUE_PROMPT_BINDING_KEY, {
        configurable: true,
        value: binding,
    });

    api.queuePrompt = wrappedQueuePrompt;
    return binding;
}

export async function interruptHostExecution(app: any = null): Promise<boolean> {
    const runtimeApp = app || _app || getComfyApp();
    const liveApi = runtimeApp?.api && typeof runtimeApp.api.interrupt === "function" ? runtimeApp.api : null;
    const api = liveApi || getRawHostApi(runtimeApp);
    if (api && typeof api.interrupt === "function") {
        await api.interrupt();
        return true;
    }
    if (api && typeof api.fetchApi === "function") {
        const resp = await api.fetchApi("/interrupt", { method: "POST" });
        if (!resp?.ok) throw new Error(`POST /interrupt failed (${resp?.status})`);
        return true;
    }
    const resp = await fetch("/interrupt", { method: "POST", credentials: "include" });
    if (!resp.ok) throw new Error(`POST /interrupt failed (${resp.status})`);
    return false;
}

function _getHostCanvas(app: any = null): any {
    const runtimeApp = app || _app || getComfyApp();
    return runtimeApp?.canvas || null;
}

export function getHostCanvas(app: any = null): any {
    return _getHostCanvas(app);
}

function _getHostGraph(app: any = null): any {
    const runtimeApp = app || _app || getComfyApp();
    return runtimeApp?.graph || runtimeApp?.canvas?.graph || null;
}

export function getHostGraph(app: any = null): any {
    return _getHostGraph(app);
}

function _cloneHostQueuedWidgetValue<T>(value: T): T {
    if (value == null || typeof value !== "object") return value;
    try {
        return typeof structuredClone === "function"
            ? structuredClone(value)
            : JSON.parse(JSON.stringify(value));
    } catch {
        return value;
    }
}

function _walkHostGraphNodes(graph: any, callback: (node: any) => void) {
    walkGraphNodes(graph, ({ node }) => callback(node));
}

function _captureHostQueuedWidgetSnapshots(graph: any): Array<{ widget: any; value: any }> {
    const snapshots: Array<{ widget: any; value: any }> = [];
    _walkHostGraphNodes(graph, (node: any) => {
        for (const widget of node?.widgets ?? []) {
            snapshots.push({ widget, value: _cloneHostQueuedWidgetValue(widget?.value) });
        }
    });
    return snapshots;
}

function _restoreHostQueuedWidgetSnapshots(
    app: any,
    snapshots: Array<{ widget: any; value: any }> | null,
) {
    for (const entry of Array.isArray(snapshots) ? snapshots : []) {
        const widget = entry?.widget;
        if (!widget || typeof widget !== "object") continue;
        const restoredValue = _cloneHostQueuedWidgetValue(entry?.value);
        try {
            widget.value = restoredValue;
        } catch (e) {
            console.debug?.(e);
            continue;
        }
        try {
            widget.callback?.(restoredValue);
        } catch (e) {
            console.debug?.(e);
        }
    }
    refreshHostCanvasGraph(app, { change: false });
}

function _resolveHostClientId(api: any, app: any): string {
    const candidates = [
        api?.clientId,
        api?.clientID,
        api?.client_id,
        app?.clientId,
        app?.clientID,
        app?.client_id,
    ];
    for (const value of candidates) {
        const safe = String(value || "").trim();
        if (safe) return safe;
    }
    return "";
}

function _readSelectedCanvasCollection(app: any = null): any {
    const canvas = _getHostCanvas(app);
    return canvas?.selected_nodes ?? canvas?.selectedNodes ?? null;
}

export function getSelectedHostCanvasNodes(app: any = null): any[] {
    const selected = _readSelectedCanvasCollection(app);
    if (!selected) return [];
    if (Array.isArray(selected)) return selected.filter(Boolean);
    if (selected instanceof Map) return Array.from(selected.values()).filter(Boolean);
    if (typeof selected === "object") return Object.values(selected).filter(Boolean);
    return [];
}

export function getSelectedHostCanvasNodeIds(app: any = null): string[] {
    return getSelectedHostCanvasNodes(app)
        .map((node: any) => String(node?.id ?? "").trim())
        .filter(Boolean);
}

export function getFirstSelectedHostCanvasNode(app: any = null): any {
    return getSelectedHostCanvasNodes(app)[0] || null;
}

export function refreshHostCanvasGraph(app: any = null, options: { draw?: boolean; change?: boolean } = {}): boolean {
    try {
        const runtimeApp = app || _app || getComfyApp();
        const canvas = _getHostCanvas(runtimeApp);
        const graph = _getHostGraph(runtimeApp);
        canvas?.setDirty?.(true, true);
        if (options.draw !== false) {
            canvas?.draw?.(true, true);
        }
        graph?.setDirtyCanvas?.(true, true);
        if (options.change !== false) {
            graph?.change?.();
        }
        return Boolean(canvas || graph);
    } catch (e) {
        console.debug?.(e);
        return false;
    }
}

export function notifyHostNodeGraphChanged(
    node: any,
    app: any = null,
    options: { draw?: boolean; change?: boolean } = {},
): boolean {
    try {
        const runtimeApp = app || _app || getComfyApp();
        const rootGraph = _getHostGraph(runtimeApp);
        const nodeGraph = node?.graph ?? null;
        if (nodeGraph && nodeGraph !== rootGraph) {
            nodeGraph.setDirtyCanvas?.(true, true);
            if (options.change !== false) {
                nodeGraph.change?.();
            }
        }
        return refreshHostCanvasGraph(runtimeApp, options);
    } catch (e) {
        console.debug?.(e);
        return false;
    }
}

export function addNodeToHostGraph(node: any, app: any = null): boolean {
    if (!node) return false;
    try {
        const runtimeApp = app || _app || getComfyApp();
        const graph = _getHostGraph(runtimeApp);
        if (!graph || typeof graph.add !== "function") return false;
        graph.add(node);
        notifyHostNodeGraphChanged(node, runtimeApp);
        return true;
    } catch (e) {
        console.debug?.(e);
        return false;
    }
}

function _getHostLiteGraph(app: any = null): any {
    try {
        const runtimeApp = app || _app || getComfyApp();
        const root = globalThis as typeof globalThis & {
            LiteGraph?: any;
            window?: { LiteGraph?: any };
        };
        return (
            runtimeApp?.LiteGraph ||
            runtimeApp?.ui?.LiteGraph ||
            runtimeApp?.canvas?.LiteGraph ||
            root?.LiteGraph ||
            root?.window?.LiteGraph ||
            null
        );
    } catch (e) {
        console.debug?.(e);
        return null;
    }
}

export function createHostNode(nodeType: any, app: any = null): any {
    const type = String(nodeType || "").trim();
    if (!type) return null;
    try {
        const LiteGraph = _getHostLiteGraph(app);
        if (!LiteGraph || typeof LiteGraph.createNode !== "function") return null;
        return LiteGraph.createNode(type) || null;
    } catch (e) {
        console.debug?.(e);
        return null;
    }
}

export function importWorkflowIntoHostCanvas(workflow: any, app: any = null): boolean {
    const runtimeApp = app || _app || getComfyApp();
    if (!workflow || typeof workflow !== "object") return false;
    try {
        const importCandidates = [
            runtimeApp?.importWorkflow,
            runtimeApp?.loadWorkflow,
            runtimeApp?.workflowManager?.importWorkflow,
            runtimeApp?.workflowManager?.loadWorkflow,
            runtimeApp?.extensionManager?.workflow?.importWorkflow,
        ];
        for (const candidate of importCandidates) {
            if (typeof candidate !== "function") continue;
            try {
                candidate.call(runtimeApp?.workflowManager || runtimeApp?.extensionManager?.workflow || runtimeApp, workflow);
                refreshHostCanvasGraph(runtimeApp, { draw: false, change: false });
                return true;
            } catch (e) {
                console.debug?.(e);
            }
        }
        if (typeof runtimeApp?.loadGraphData === "function") {
            runtimeApp.loadGraphData(workflow);
            return true;
        }
        const graph = _getHostGraph(runtimeApp);
        if (typeof graph?.configure === "function") {
            graph.configure(workflow);
            refreshHostCanvasGraph(runtimeApp, { draw: false, change: false });
            return true;
        }
    } catch (e) {
        console.debug?.(e);
    }
    return false;
}

/**
 * Import a workflow through the stable graph loading path.
 * New-tab APIs vary across ComfyUI frontend builds, so this avoids guessing
 * private tab methods that may not switch the active graph synchronously.
 */
export function importWorkflowPreferHostTab(
    workflow: any,
    app: any = null,
): { ok: boolean; mode: "new-tab" | "replace" | "none" } {
    const runtimeApp = app || _app || getComfyApp();
    if (!workflow || typeof workflow !== "object") return { ok: false, mode: "none" };
    try {
        if (importWorkflowIntoHostCanvas(workflow, runtimeApp)) {
            return { ok: true, mode: "replace" };
        }
    } catch (e) {
        console.debug?.(e);
    }
    return { ok: false, mode: "none" };
}

export function serializeCurrentHostWorkflow(app: any = null): any {
    try {
        const runtimeApp = app || _app || getComfyApp();
        if (typeof runtimeApp?.graphToPrompt === "function") {
            const data = runtimeApp.graphToPrompt();
            if (data?.workflow && typeof data.workflow === "object") return data.workflow;
        }
        const graph = _getHostGraph(runtimeApp);
        if (typeof graph?.serialize === "function") {
            const workflow = graph.serialize();
            if (workflow && typeof workflow === "object") return workflow;
        }
        const rootGraph = runtimeApp?.rootGraph || runtimeApp?.graph?.rootGraph || null;
        if (typeof rootGraph?.serialize === "function") {
            const workflow = rootGraph.serialize();
            if (workflow && typeof workflow === "object") return workflow;
        }
    } catch (e) {
        console.debug?.(e);
    }
    return null;
}

/**
 * Best-effort host dirty-state probe.
 * Returns:
 *   - true / false when a reliable host signal is available
 *   - null when no compatible signal can be detected
 */
export function isHostWorkflowDirty(app: any = null): boolean | null {
    try {
        const runtimeApp = app || _app || getComfyApp();
        if (!runtimeApp) return null;

        const candidates = [
            runtimeApp?.isDirty,
            runtimeApp?.dirty,
            runtimeApp?.graph?.isDirty,
            runtimeApp?.graph?.dirty,
            runtimeApp?.graph?.is_modified,
            runtimeApp?.graph?.modified,
            runtimeApp?.graph?.has_changed,
            runtimeApp?.graph?.changed,
        ];
        for (const candidate of candidates) {
            if (typeof candidate === "boolean") return candidate;
            if (typeof candidate === "number") return candidate !== 0;
            if (typeof candidate === "function") {
                try {
                    const value = candidate.call(runtimeApp?.graph || runtimeApp);
                    if (typeof value === "boolean") return value;
                    if (typeof value === "number") return value !== 0;
                } catch (e) {
                    console.debug?.(e);
                }
            }
        }
    } catch (e) {
        console.debug?.(e);
    }
    return null;
}

export async function queueHostPrompt(
    options: {
        app?: any;
        forceNativeQueue?: boolean;
        resolvePromptData?: ((app: any) => any | Promise<any>) | null;
        enrichPromptData?: ((promptData: any) => any) | null;
        buildPromptRequestBody?: ((promptData: any, context: { clientId: string }) => any) | null;
    } = {},
): Promise<boolean> {
    const app = options.app || _app || getComfyApp();
    if (!app) throw new Error("ComfyUI app not available");

    const api = getRawHostApi(app);
    const hasApiPath = Boolean(
        (api && typeof api.queuePrompt === "function") ||
            (api && typeof api.fetchApi === "function"),
    );

    if ((options.forceNativeQueue || !hasApiPath) && typeof app.queuePrompt === "function") {
        await app.queuePrompt(0);
        return true;
    }

    const rootGraph = getHostRootGraph(app);
    let widgetSnapshots: Array<{ widget: any; value: any }> | null = null;
    try {
        widgetSnapshots = _captureHostQueuedWidgetSnapshots(rootGraph);
        _walkHostGraphNodes(rootGraph, (node: any) => {
            for (const widget of node?.widgets ?? []) {
                widget.beforeQueued?.({ isPartialExecution: false });
            }
        });

        const resolver =
            typeof options.resolvePromptData === "function"
                ? options.resolvePromptData
                : (runtimeApp: any) => runtimeApp?.graphToPrompt?.();
        const promptData = await resolver(app);
        if (!promptData?.output) throw new Error("graphToPrompt returned empty output");

        const enriched =
            typeof options.enrichPromptData === "function"
                ? options.enrichPromptData(promptData)
                : promptData;

        if (api && typeof api.queuePrompt === "function") {
            await api.queuePrompt(0, enriched);
            _walkHostGraphNodes(rootGraph, (node: any) => {
                for (const widget of node?.widgets ?? []) {
                    widget.afterQueued?.({ isPartialExecution: false });
                }
            });
            refreshHostCanvasGraph(app, { change: false });
            return true;
        }

        const bodyBuilder =
            typeof options.buildPromptRequestBody === "function"
                ? options.buildPromptRequestBody
                : (pd: any, context: { clientId: string }) => {
                      const body: Record<string, any> = {
                          prompt: pd?.output,
                          extra_data: pd?.extra_data || {},
                      };
                      if (context.clientId) body.client_id = context.clientId;
                      return body;
                  };
        const body = bodyBuilder(promptData, { clientId: _resolveHostClientId(api, app) });

        if (api && typeof api.fetchApi === "function") {
            const resp = await api.fetchApi("/prompt", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(body),
            });
            if (!resp?.ok) throw new Error(`POST /prompt failed (${resp?.status})`);
            _walkHostGraphNodes(rootGraph, (node: any) => {
                for (const widget of node?.widgets ?? []) {
                    widget.afterQueued?.({ isPartialExecution: false });
                }
            });
            refreshHostCanvasGraph(app, { change: false });
            return true;
        }

        const resp = await fetch("/prompt", {
            method: "POST",
            credentials: "include",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
        });
        if (!resp.ok) throw new Error(`POST /prompt failed (${resp.status})`);
        _walkHostGraphNodes(rootGraph, (node: any) => {
            for (const widget of node?.widgets ?? []) {
                widget.afterQueued?.({ isPartialExecution: false });
            }
        });
        refreshHostCanvasGraph(app, { change: false });
        return false;
    } catch (error) {
        _restoreHostQueuedWidgetSnapshots(app, widgetSnapshots);
        throw error;
    }
}

export function focusHostCanvasNode(
    node: any,
    options: { app?: any; select?: boolean; focusCanvas?: boolean } = {},
): boolean {
    if (!node) return false;
    try {
        const runtimeApp = options.app || _app || getComfyApp();
        const canvas = _getHostCanvas(runtimeApp);
        if (!canvas) return false;
        if (options.select !== false) {
            canvas.selectNode?.(node, false);
        }
        if (typeof canvas.centerOnNode === "function") {
            canvas.centerOnNode(node);
        } else if (node.pos && canvas.ds) {
            const canvasEl = canvas.canvas || canvas.element || null;
            const width = Number(canvasEl?.width || canvasEl?.clientWidth || 800) || 800;
            const height = Number(canvasEl?.height || canvasEl?.clientHeight || 600) || 600;
            const scale = Number(canvas.ds?.scale || 1) || 1;
            const nodeWidth = Number(node?.size?.[0] || 100) || 100;
            const nodeHeight = Number(node?.size?.[1] || 80) || 80;
            const offsetX = -Number(node.pos[0] || 0) + width / (2 * scale) - nodeWidth / 2;
            const offsetY = -Number(node.pos[1] || 0) + height / (2 * scale) - nodeHeight / 2;
            if (Array.isArray(canvas.ds.offset)) {
                canvas.ds.offset[0] = offsetX;
                canvas.ds.offset[1] = offsetY;
            } else if (canvas.ds.offset && typeof canvas.ds.offset === "object") {
                canvas.ds.offset.x = offsetX;
                canvas.ds.offset.y = offsetY;
            }
            canvas.setDirty?.(true, true);
        }
        if (options.focusCanvas !== false) {
            canvas.canvas?.focus?.();
        }
        return true;
    } catch (e) {
        console.debug?.(e);
        return false;
    }
}

export function centerHostCanvasNodeById(nodeId: any, app: any = null): boolean {
    const safeNodeId = String(nodeId || "").trim();
    if (!safeNodeId) return false;
    try {
        const runtimeApp = app || _app || getComfyApp();
        const node = findGraphNodeById(getHostRootGraph(runtimeApp), safeNodeId);
        if (!node) return false;
        return focusHostCanvasNode(node, { app: runtimeApp, select: false, focusCanvas: false });
    } catch (e) {
        console.debug?.(e);
        return false;
    }
}

// -- graph/canvas helpers -----------------------------------------------------

function _getMainCanvasState() {
    const app = _app || getComfyApp() || null;
    const graphCanvas = app?.canvas || null;
    const ds = graphCanvas?.ds || null;
    const canvas = graphCanvas?.canvas || graphCanvas?.el || null;
    if (!graphCanvas || !ds || !canvas) return null;
    const scale = Number(ds?.scale);
    const width = Number(canvas?.width || canvas?.clientWidth || 0);
    const height = Number(canvas?.height || canvas?.clientHeight || 0);
    if (!Number.isFinite(scale) || scale <= 0 || !(width > 0) || !(height > 0)) return null;
    return { app, graphCanvas, ds, scale, width, height };
}

function _setCanvasOffset(ds: any, x: any, y: any) {
    if (Array.isArray(ds?.offset)) {
        ds.offset[0] = x;
        ds.offset[1] = y;
        return true;
    }
    if (ds?.offset && typeof ds.offset === "object") {
        ds.offset.x = x;
        ds.offset.y = y;
        return true;
    }
    return false;
}

function _markCanvasDirty(app: any, graphCanvas: any) {
    try {
        graphCanvas?.setDirty?.(true, true);
    } catch (e) {
        console.debug?.(e);
    }
    try {
        app?.graph?.setDirtyCanvas?.(true, true);
    } catch (e) {
        console.debug?.(e);
    }
}

/**
 * Center the ComfyUI graph canvas on a world-space point.
 *
 * @param {{ x: number, y: number }} worldPoint
 * @returns {boolean}
 */
export function centerGraphCanvasOnWorldPoint(worldPoint: any) {
    try {
        const state = _getMainCanvasState();
        if (!state || !worldPoint) return false;
        const pointX = Number(worldPoint.x);
        const pointY = Number(worldPoint.y);
        if (!Number.isFinite(pointX) || !Number.isFinite(pointY)) return false;
        const dpr = Math.max(
            1,
            Number(globalThis?.devicePixelRatio ?? globalThis?.window?.devicePixelRatio) || 1,
        );
        const offsetX = -pointX + state.width * 0.5 / (state.scale * dpr);
        const offsetY = -pointY + state.height * 0.5 / (state.scale * dpr);
        if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY)) return false;
        if (!_setCanvasOffset(state.ds, offsetX, offsetY)) return false;
        _markCanvasDirty(state.app, state.graphCanvas);
        return true;
    } catch (e) {
        console.debug?.(e);
        return false;
    }
}

/**
 * Return the current graph viewport in workflow/world coordinates.
 *
 * @returns {{ x0: number, y0: number, x1: number, y1: number } | null}
 */
export function getGraphCanvasViewportBounds() {
    try {
        const state = _getMainCanvasState();
        if (!state) return null;
        const off = state.ds?.offset;
        const offX = Array.isArray(off) ? Number(off[0]) : Number(off?.x);
        const offY = Array.isArray(off) ? Number(off[1]) : Number(off?.y);
        if (!Number.isFinite(offX) || !Number.isFinite(offY)) return null;
        return {
            x0: -offX / state.scale,
            y0: -offY / state.scale,
            x1: (state.width - offX) / state.scale,
            y1: (state.height - offY) / state.scale,
        };
    } catch (e) {
        console.debug?.(e);
        return null;
    }
}

// -- public API ----------------------------------------------------------------

export const hostAdapter = {
    init,
    isReady,
    ready,
    showToast,
    getHostToastApi,
    confirm,
    getHostDialogApi,
    getSetting,
    getSettingForApp,
    setSetting,
    setSettingForApp,
    getHostSettingsApi,
    registerSidebarTab,
    registerSidebarTabForApp,
    registerBottomPanelTab,
    registerBottomPanelTabForApp,
    activateSidebarTab,
    activateSidebarTabForApp,
    registerCommand,
    registerCommandForApp,
    registerKeybinding,
    registerKeybindingForApp,
    getHostExtensionManager,
    getRawHostApp,
    getRawHostApi,
    waitForRawHostApi,
    fetchHostApi,
    queueHostPrompt,
    observeHostCanvasSelection,
    wrapHostQueuePrompt,
    interruptHostExecution,
    getSelectedHostCanvasNodes,
    getSelectedHostCanvasNodeIds,
    getFirstSelectedHostCanvasNode,
    getHostCanvas,
    getHostGraph,
    refreshHostCanvasGraph,
    notifyHostNodeGraphChanged,
    importWorkflowIntoHostCanvas,
    importWorkflowPreferHostTab,
    isHostWorkflowDirty,
    serializeCurrentHostWorkflow,
    addNodeToHostGraph,
    createHostNode,
    focusHostCanvasNode,
    centerHostCanvasNodeById,
    centerGraphCanvasOnWorldPoint,
    getGraphCanvasViewportBounds,
};

export default hostAdapter;
