/**
 * Entry Point  -  Majoor Assets Manager
 *
 * Registers the ComfyUI extension and wires up the Vue-based UI.
 *
 * Vue migration summary
 * ---------------------
 * - Sidebar and bottom-panel tabs mount isolated Vue 3 + Pinia applications
 *   (see ui/vue/App.vue and ui/vue/GeneratedFeedApp.vue).
 * - Keep-alive is handled by mountKeepAlive() in createVueApp.js - no more
 *   _mjrKeepAliveMounted DOM flags.
 * - Commands are declared on the extension object (declarative API) so ComfyUI
 *   surfaces them in the palette and shortcut editor automatically.
 * - Toast notifications route through app.extensionManager.toast (ComfyUI's
 *   PrimeVue toast) with a DOM fallback - no changes needed in toast.js as it
 *   already prefers the native API.
 */

// Side-effect import: starts the very first /list request immediately when this
// module is parsed (well before ComfyUI invokes our `setup()` step 8b). Keep it
// at the very top  -  its tiny dep set (api/client + endpoints + config) ensures
// nothing heavy runs ahead of it. Result is consumed by useGridLoader.fetchPage.
import "./features/runtime/earlyFetch.js";
// Side-effect: registers the "Send to Asset Manager" right-click menu on
// SaveImage / VHS_VideoCombine etc. Isolated from the Vue runtime so a
// failure here cannot break the asset grid.
import "./integration/comfy_send_to_am.js";
import { testAPI, triggerStartupScan } from "./app/bootstrap.js";
import { checkMajoorVersion } from "./app/versionCheck.js";
import { ensureStyleLoaded } from "./app/style.js";
import { buildMajoorSettings, registerMajoorSettings } from "./app/settings.js";
import {
    getComfyApi,
    setComfyApi,
    setComfyApp,
    waitForComfyApi,
    waitForComfyApp,
} from "./app/comfyApiBridge.js";
import { EVENTS } from "./app/events.js";
import { ensureFloatingViewerProgressTracking } from "./features/viewer/floatingViewerProgress.js";
import {
    NODE_STREAM_FEATURE_ENABLED,
    NODE_STREAM_REACTIVATION_DOC,
} from "./features/viewer/nodeStream/nodeStreamFeatureFlag.js";
import {
    loadAssets,
    upsertAsset,
    upsertAssetNow,
    removeAssetsFromGrid,
} from "./features/grid/gridApi.js";
import { getActiveGridContainer } from "./features/panel/panelRuntimeRefs.js";
import {
    pushGeneratedAsset,
    refreshGeneratedFeedHosts,
} from "./features/bottomPanel/feed/feedHost.js";
import { createExecutionRuntimeController } from "./features/stacks/executionRuntimeController.js";
import { extractOutputFiles } from "./utils/extractOutputFiles.js";
import { post } from "./api/client.js";
import { ENDPOINTS } from "./api/endpoints.js";
import { comfyToast } from "./app/toast.js";
import { t } from "./app/i18n.js";
import { APP_CONFIG } from "./app/config.js";
import {
    getRuntimeEnrichmentState,
    setRuntimeEnrichmentState,
} from "./stores/runtimeEnrichmentState.js";
import { reportError, mjrDbg } from "./utils/logging.js";
// @ts-ignore - external ComfyUI module
import { app } from "../../scripts/app.js";
import { registerRealtimeListeners } from "./features/runtime/registerRealtimeListeners.js";
import { exposeDebugApis } from "./features/runtime/entryDebugApi.js";
import { installBenignConsoleNoiseFilter } from "./features/runtime/benignConsoleNoise.js";
import { registerGenInfoOverridePicker } from "./features/geninfo/genInfoOverridePicker.js";
import {
    buildAboutPageBadges,
    buildBottomPanelTabs,
    buildNativeCommands,
    buildNativeKeybindings,
    buildNativeMenuCommands,
    getMajoorNodeMenuItems,
    getMajoorCanvasMenuItems,
    getMajoorSelectionToolboxCommands,
    mountGlobalRuntime,
    prewarmAssetsSidebar,
    registerAssetsSidebar,
    registerNativeCommands,
    registerNativeKeybindings,
    startEarlyFetch,
    teardownTopBarMfvButton,
    teardownGeneratedFeed,
    teardownAssetsSidebar,
    teardownGlobalRuntime,
} from "./features/runtime/entryUiRegistration.js";
import {
    ENTRY_RUNTIME_KEY,
    cleanupEntryRuntime,
    installEntryRuntimeController,
    registerCleanableListener,
    removeApiHandlers,
    removeRuntimeWindowHandlers,
} from "./features/runtime/entryRuntimeLifecycle.js";

// -- lazy module handles -------------------------------------------------------

/** @type {import("./features/viewer/LiveStreamTracker.js") | null} */
let liveStreamModule = null;
/** @type {import("./features/viewer/nodeStream/NodeStreamController.js") | null} */
let nodeStreamModule = null;
/** @type {Promise<import("./features/viewer/floatingViewerManager.js")> | null} */
let floatingViewerManagerModulePromise = null;

function loadFloatingViewerManagerModule() {
    if (!floatingViewerManagerModulePromise) {
        floatingViewerManagerModulePromise = import("./features/viewer/floatingViewerManager.js");
    }
    return floatingViewerManagerModulePromise;
}

function teardownLoadedFloatingViewerManager() {
    if (!floatingViewerManagerModulePromise) return;
    void floatingViewerManagerModulePromise
        .then((mod) => mod?.teardownFloatingViewerManager?.())
        .catch((e) => console.debug?.("[Majoor] MFV teardown skipped", e));
}

// -- constants -----------------------------------------------------------------

const SIDEBAR_TAB_ID = "majoor-assets";
const EXECUTION_RUNTIME_KEY = "__MJR_EXECUTION_RUNTIME__";
const EXTENSION_NAME = "Majoor.AssetsManager";
let _lastExecutionBackendSync = { active: null, promptId: "" };
let _deferredGridReloadTimer = null;
const NODE_STREAM_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const NODE_STREAM_HASH_RE = /^[0-9a-f]{20,}$/i;

function _nodeStreamFirstText(...values) {
    for (const value of values) {
        const text = String(value || "").trim();
        if (text) return text;
    }
    return "";
}

function _isOpaqueNodeStreamType(type) {
    const text = String(type || "").trim();
    return NODE_STREAM_UUID_RE.test(text) || NODE_STREAM_HASH_RE.test(text);
}

function _getNodeStreamTitle(node) {
    return _nodeStreamFirstText(
        node?.title,
        node?.properties?.title,
        node?.properties?.name,
        node?.properties?.label,
        node?.name,
    );
}

function _getNodeStreamGraphNodes(graph) {
    if (!graph || typeof graph !== "object") return [];
    if (Array.isArray(graph.nodes)) return graph.nodes.filter(Boolean);
    if (Array.isArray(graph._nodes)) return graph._nodes.filter(Boolean);
    const byId = graph._nodes_by_id ?? graph.nodes_by_id ?? null;
    if (byId instanceof Map) return Array.from(byId.values()).filter(Boolean);
    if (byId && typeof byId === "object") return Object.values(byId).filter(Boolean);
    return [];
}

function _nodeStreamHasGraphNodes(graph) {
    return _getNodeStreamGraphNodes(graph).length > 0;
}

function _isNodeStreamSubgraph(node) {
    if (!node || typeof node !== "object") return false;
    const candidates = [
        node.subgraph,
        node._subgraph,
        node.subgraph?.graph,
        node.subgraph?.lgraph,
        node.properties?.subgraph,
        node.subgraph_instance,
        node.subgraph_instance?.graph,
        node.inner_graph,
        node.subgraph_graph,
    ];
    if (candidates.some(_nodeStreamHasGraphNodes)) return true;
    if (Array.isArray(node.nodes) && node.nodes.length > 0 && node.nodes !== node.graph?.nodes) {
        return true;
    }
    return _isOpaqueNodeStreamType(node.type) && Boolean(_getNodeStreamTitle(node));
}

function _getNodeStreamClassLabel(node, fallbackClassType) {
    const type = String(node?.type || fallbackClassType || "").trim();
    const title = _getNodeStreamTitle(node);
    if (_isOpaqueNodeStreamType(type)) return title || "Subgraph";
    return type || title || "Node";
}

// -- execution runtime helpers -------------------------------------------------

function ensureExecutionRuntime() {
    try {
        if (typeof window === "undefined") {
            return {
                active_prompt_id: null,
                queue_remaining: null,
                progress_node: null,
                progress_value: null,
                progress_max: null,
                cached_nodes: [],
            };
        }
        if (!window[EXECUTION_RUNTIME_KEY] || typeof window[EXECUTION_RUNTIME_KEY] !== "object") {
            window[EXECUTION_RUNTIME_KEY] = {
                active_prompt_id: null,
                queue_remaining: null,
                progress_node: null,
                progress_value: null,
                progress_max: null,
                cached_nodes: [],
            };
        }
        return window[EXECUTION_RUNTIME_KEY];
    } catch (e) {
        console.debug?.(e);
        return {};
    }
}

function emitRuntimeStatus(partial = {}) {
    try {
        const runtime = ensureExecutionRuntime();
        Object.assign(runtime, partial || {});
        window.dispatchEvent(new CustomEvent(EVENTS.RUNTIME_STATUS, { detail: { ...(runtime as any) } }));
    } catch (e) {
        console.debug?.(e);
    }
}

async function syncExecutionBackendState({ active, promptId = "" }: Record<string, any> = {}) {
    const nextActive = !!active;
    const nextPromptId = String(promptId || "").trim();
    if (
        _lastExecutionBackendSync.active === nextActive &&
        _lastExecutionBackendSync.promptId === nextPromptId
    ) {
        return;
    }
    _lastExecutionBackendSync = { active: nextActive, promptId: nextPromptId };
    try {
        await post(ENDPOINTS.RUNTIME_EXECUTION, {
            active: nextActive,
            prompt_id: nextPromptId || undefined,
            cooldown_ms: Number(APP_CONFIG.EXECUTION_IDLE_GRACE_MS) || 6000,
        });
    } catch (e) {
        reportError(e, "entry.execution_state_sync");
    }
}

function isExecutionActive() {
    try {
        return !!String((ensureExecutionRuntime() as any)?.active_prompt_id || "").trim();
    } catch (e) {
        console.debug?.(e);
        return false;
    }
}

function scheduleGridReloadWhenIdle(delayMs = 1200) {
    try {
        if (_deferredGridReloadTimer) {
            clearTimeout(_deferredGridReloadTimer);
        }
    } catch (e) {
        console.debug?.(e);
    }
    _deferredGridReloadTimer = setTimeout(
        () => {
            _deferredGridReloadTimer = null;
            if (isExecutionActive()) {
                scheduleGridReloadWhenIdle(delayMs);
                return;
            }
            const grid = getActiveGridContainer();
            if (grid) {
                loadAssets(grid);
            }
        },
        Math.max(250, Number(delayMs) || 0),
    );
}

// -- lazy module initialisation -------------------------------------------------

function setupLazyModules(runtimeApp) {
    void import("./features/viewer/LiveStreamTracker.js")
        .then((mod) => {
            liveStreamModule = mod;
            try {
                mod.initLiveStreamTracker(runtimeApp);
            } catch (e) {
                console.warn("[MJR setup] initLiveStreamTracker failed:", e);
            }
        })
        .catch((e) => console.warn("[MJR setup] LiveStreamTracker load failed:", e));

    if (!NODE_STREAM_FEATURE_ENABLED) {
        console.debug("[Majoor] Node Stream disabled by feature flag.");
        return;
    }

    void import("./features/viewer/nodeStream/NodeStreamController.js")
        .then((mod) => {
            nodeStreamModule = mod;
            try {
                mod.initNodeStream({
                    app: runtimeApp,
                    onOutput: (fileData) => {
                        void loadFloatingViewerManagerModule()
                            .then((mfv) => mfv?.floatingViewerManager?.feedNodeStream?.(fileData))
                            .catch((e) => console.debug?.("[NodeStream] MFV output failed", e));
                    },
                    onStatus: (nodeId, classType) => {
                        try {
                            const graph = runtimeApp?.graph ?? runtimeApp?.canvas?.graph ?? null;
                            const node = graph?.getNodeById?.(Number(nodeId));
                            const isSubgraph = _isNodeStreamSubgraph(node);
                            const title = _getNodeStreamTitle(node);
                            const displayClass = isSubgraph
                                ? "Subgraph"
                                : _getNodeStreamClassLabel(node, classType);
                            void loadFloatingViewerManagerModule()
                                .then((mfv) => {
                                    mfv?.floatingViewerManager?.setNodeStreamSelection?.(
                                        nodeId,
                                        displayClass,
                                        isSubgraph ? title || _getNodeStreamClassLabel(node, classType) : title,
                                    );
                                })
                                .catch((err) => console.debug?.("[NodeStream] MFV status failed", err));
                        } catch (err) {
                            console.debug?.("[NodeStream] onStatus failed", err);
                        }
                    },
                });
            } catch (e) {
                console.warn("[MJR setup] initNodeStream failed:", e);
            }
        })
        .catch((e) => console.warn("[MJR setup] NodeStream load failed:", e));
}

// -- API listener setup --------------------------------------------------------

async function setupApiListeners(runtimeApp, executionRuntime) {
    const api =
        (await waitForComfyApi({ app: runtimeApp, timeoutMs: 4000 })) || getComfyApi(runtimeApp);
    setComfyApi(api || null);
    if (!api) {
        console.warn("Majoor API not available, real-time updates disabled");
        return;
    }
    void ensureFloatingViewerProgressTracking({ api, app: runtimeApp }).catch((e) =>
        console.debug?.("[Majoor] MFV progress tracking init failed", e),
    );

    let runtime = null;
    try {
        runtime = typeof window !== "undefined" ? window[ENTRY_RUNTIME_KEY] || null : null;
    } catch (e) {
        console.debug?.(e);
    }

    removeApiHandlers(runtime?.api || null, reportError);
    if (api !== runtime?.api) removeApiHandlers(api, reportError);
    removeRuntimeWindowHandlers(runtime, reportError);

    await registerRealtimeListeners({
        api,
        runtime,
        executionRuntime,
        appRef: app,
        liveStreamModule,
        ensureExecutionRuntime,
        emitRuntimeStatus,
        getActiveGridContainer,
        pushGeneratedAsset,
        upsertAsset,
        upsertAssetNow,
        removeAssetsFromGrid,
        getEnrichmentState: getRuntimeEnrichmentState,
        setEnrichmentState: setRuntimeEnrichmentState,
        comfyToast,
        t,
        reportError,
        registerCleanableListener,
        syncExecutionBackendState,
    });
}

// -- execution runtime controller ----------------------------------------------

const executionRuntime = createExecutionRuntimeController({
    post,
    ENDPOINTS,
    reportError,
    extractOutputFiles,
    ensureExecutionRuntime,
    emitRuntimeStatus,
    refreshGeneratedFeedHosts,
    getActiveGridContainer,
});

// -- extension registration -----------------------------------------------------
// Keep registration unconditional. ComfyUI reload needs setup() to run again,
// and installEntryRuntimeController() already tears down the previous runtime.
app.registerExtension({
    name: EXTENSION_NAME,

    /**
     * Declarative settings  -  registered with ComfyUI's settings panel.
     * Replaces the imperative registerMajoorSettings() call for the settings UI.
     */
    settings: buildMajoorSettings(app),

    /**
     * Declarative commands  -  ComfyUI surfaces these in the command palette
     * and the shortcut editor without additional imperative calls.
     */
    commands: buildNativeCommands(app, {
        sidebarTabId: SIDEBAR_TAB_ID,
        triggerStartupScan,
    }),
    keybindings: buildNativeKeybindings(),
    menuCommands: buildNativeMenuCommands(),
    aboutPageBadges: buildAboutPageBadges(),

    /**
     * Declarative bottom-panel tabs  -  processed by ComfyUI's extension service.
     * Each tab's render/destroy callbacks mount/unmount the Vue app via
     * mountKeepAlive() so state survives panel collapse/expand cycles.
     */
    bottomPanelTabs: buildBottomPanelTabs(),

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

    async setup() {
        // 1. Runtime lifecycle controller (handles extension cleanup on reload).
        installEntryRuntimeController({
            cleanupEntryRuntimeFn: cleanupEntryRuntime,
            teardownLiveStreamTracker: (runtimeApp) =>
                liveStreamModule?.teardownLiveStreamTracker(runtimeApp),
            teardownNodeStream: (runtimeApp) => nodeStreamModule?.teardownNodeStream(runtimeApp),
            teardownFloatingViewerManager: teardownLoadedFloatingViewerManager,
            teardownGeneratedFeed,
            teardownAssetsSidebar,
            teardownGlobalRuntime,
            teardownTopBarMfvButton,
            reportError,
        });
        installBenignConsoleNoiseFilter();

        // 2. Resolve the ComfyUI app reference (may take a few frames to be ready).
        setComfyApp(app);
        const runtimeApp = (await waitForComfyApp({ timeoutMs: 12000 })) || app;
        setComfyApp(runtimeApp);
        registerGenInfoOverridePicker(runtimeApp);
        try {
            if (typeof window !== "undefined") (window as any).__MJR_RUNTIME_APP__ = runtimeApp;
        } catch (e) {
            console.debug?.(e);
        }

        // 3. Baseline checks.
        testAPI();
        ensureStyleLoaded({ enabled: true });
        mountGlobalRuntime();

        // 4. Global runtime  -  viewer + DnD stay mounted from the always-on Vue runtime.

        // 5. Lazy viewer modules (non-blocking).
        setupLazyModules(runtimeApp);

        // 6. Settings runtime init (applies settings to APP_CONFIG, starts watcher sync...).
        registerMajoorSettings(runtimeApp, () => {
            const grid = getActiveGridContainer();
            if (!grid) return;
            if (APP_CONFIG.DEFER_GRID_FETCH_DURING_EXECUTION && isExecutionActive()) {
                scheduleGridReloadWhenIdle();
                return;
            }
            loadAssets(grid);
        });

        // 7. Fallback command registration for older ComfyUI versions that don't
        //    process the declarative `commands` array on the extension object.
        registerNativeCommands(runtimeApp, {
            sidebarTabId: SIDEBAR_TAB_ID,
            triggerStartupScan,
        });
        registerNativeKeybindings(runtimeApp);

        // 8. Version check (deferred so it doesn't slow startup).
        setTimeout(() => {
            void checkMajoorVersion();
        }, 5000);

        // 9. Proactive early fetch  -  start the first assets page in the background
        //     so the data is ready (or already in-flight) when the user opens the
        //     sidebar. Fire immediately: previous 800ms timeout was wasted latency
        //     that delayed the first paint when the user clicked the sidebar fast.
        //     startEarlyFetch() is a no-op when ComfyUI is executing a prompt and
        //     its TTL is wide enough to survive slow extension registration.
        try {
            startEarlyFetch();
        } catch (e) {
            console.debug?.(e);
        }

        // 9. WebSocket / realtime listeners.
        void setupApiListeners(runtimeApp, executionRuntime).catch((e) =>
            reportError(e, "entry.api_setup"),
        );

        // 10. Register sidebar tab (Vue app mounts on first open via mountKeepAlive).
        if (
            registerAssetsSidebar(runtimeApp, {
                sidebarTabId: SIDEBAR_TAB_ID,
            })
        ) {
            mjrDbg("[Majoor] Sidebar tab registered (Vue)");
        } else {
            console.warn(
                "Majoor Assets Manager: extensionManager.registerSidebarTab is unavailable",
            );
        }

        // 10b. Prewarm the sidebar Vue app on a detached host during browser
        // idle so the click->cards latency drops from ~2 s (cold mount) to one
        // DOM frame (host re-attach). See entryUiRegistration.prewarmAssetsSidebar.
        try {
            const _ric = typeof window !== "undefined" ? window.requestIdleCallback : null;
            const _doPrewarm = () => {
                try {
                    prewarmAssetsSidebar();
                } catch (e) {
                    console.debug?.(e);
                }
            };
            if (typeof _ric === "function") {
                _ric(_doPrewarm, { timeout: 2500 });
            } else {
                setTimeout(_doPrewarm, 600);
            }
        } catch (e) {
            console.debug?.(e);
        }

        // 11. Bottom panel tab is registered declaratively via `bottomPanelTabs`.

        // 12. Startup warmup: API/DB/counters first, then idle index scan.
        // 12. Debug API surface (development/support).
        exposeDebugApis({
            resolveNodeStreamModule: async () => {
                if (!nodeStreamModule) {
                    nodeStreamModule =
                        await import("./features/viewer/nodeStream/NodeStreamController.js");
                }
                return nodeStreamModule;
            },
        });
    },

    // -- hooks ------------------------------------------------------------------

    /**
     * ComfyUI v1.42+  -  fires when individual node outputs are updated during
     * a workflow execution.  Feeds the execution-stack runtime so the bottom
     * feed panel receives live assets.
     */
    onNodeOutputsUpdated(nodeOutputs) {
        try {
            executionRuntime.handleNodeOutputsUpdated(nodeOutputs);
        } catch (e) {
            console.debug?.("[Majoor] onNodeOutputsUpdated error", e);
        }
    },

    /** Context-menu items injected into ComfyUI node right-click menus. */
    getNodeMenuItems(node) {
        return getMajoorNodeMenuItems(node, app, { sidebarTabId: SIDEBAR_TAB_ID });
    },

    /** Context-menu items injected into ComfyUI canvas right-click menus. */
    getCanvasMenuItems(canvas) {
        void canvas;
        return getMajoorCanvasMenuItems(app, {
            sidebarTabId: SIDEBAR_TAB_ID,
            triggerStartupScan,
        });
    },

    /** ComfyUI v1.10.9+ selection toolbox buttons for trackable canvas nodes. */
    getSelectionToolboxCommands(selectedItem) {
        return getMajoorSelectionToolboxCommands(selectedItem);
    },
});
