import {
    centerHostCanvasNodeById,
    getRawHostApi,
    getRawHostApp,
    waitForRawHostApi,
    wrapHostQueuePrompt,
} from "../../app/hostAdapter.js";
import { findGraphNodeById, getHostRootGraph } from "../../app/graphTraversal.js";

const PROGRESS_UPDATE_EVENT = "progress-update";
const GLOBAL_SERVICE_KEY = "__MJR_MFV_PROGRESS_SERVICE__";

function getGlobalHost() {
    if (typeof globalThis !== "undefined") return globalThis;
    if (typeof window !== "undefined") return window;
    return {};
}

function makeCustomEvent(type: any, detail: any) {
    if (typeof CustomEvent === "function") {
        return new CustomEvent(type, { detail });
    }
    const event: any = typeof Event === "function" ? new Event(type) : { type };
    event.detail = detail;
    return event;
}

export class FloatingViewerPromptExecution {
    [key: string]: any;
    constructor(id: any, getApp = () => getRawHostApp()) {
        this.id = String(id || "");
        this.promptApi = null;
        this.executedNodeIds = [];
        this.totalNodes = 0;
        this.currentlyExecuting = null;
        this.errorDetails = null;
        this._getApp = typeof getApp === "function" ? getApp : () => null;
    }

    setPrompt(prompt: any) {
        const output = prompt && typeof prompt === "object" ? prompt.output : null;
        this.promptApi = output && typeof output === "object" ? output : null;
        this.totalNodes = this.promptApi ? Object.keys(this.promptApi).length : 0;
    }

    getApiNode(nodeId: any) {
        return this.promptApi?.[String(nodeId)] || null;
    }

    getNodeLabel(nodeId: any) {
        const apiNode = this.getApiNode(nodeId);
        let label = apiNode?._meta?.title || apiNode?.class_type || "";
        if (!label) {
            const app = this._getApp?.();
            const graphNode = findGraphNodeById(getHostRootGraph(app), nodeId);
            label = graphNode?.title || graphNode?.type || "";
        }
        return String(label || "").trim();
    }

    markExecuted(nodeId: any) {
        const safeNodeId = String(nodeId || "").trim();
        if (!safeNodeId) return;
        if (!this.executedNodeIds.includes(safeNodeId)) {
            this.executedNodeIds.push(safeNodeId);
        }
    }

    executing(nodeId: any, step: any, maxSteps: any) {
        if (nodeId == null) {
            this.currentlyExecuting = null;
            return;
        }
        const nextNodeId = String(nodeId || "").trim();
        if (!nextNodeId) return;

        if (this.currentlyExecuting?.nodeId !== nextNodeId) {
            if (this.currentlyExecuting != null) {
                this.markExecuted(this.currentlyExecuting.nodeId);
            }
            this.currentlyExecuting = {
                nodeId: nextNodeId,
                nodeLabel: this.getNodeLabel(nextNodeId),
                pass: 0,
            };

            const apiNode = this.getApiNode(nextNodeId);
            if (!this.currentlyExecuting.nodeLabel) {
                this.currentlyExecuting.nodeLabel = this.getNodeLabel(nextNodeId);
            }
            if (apiNode?.class_type === "UltimateSDUpscale") {
                this.currentlyExecuting.pass -= 1;
                this.currentlyExecuting.maxPasses = -1;
            } else if (apiNode?.class_type === "IterativeImageUpscale") {
                this.currentlyExecuting.maxPasses = Number(apiNode?.inputs?.steps) || -1;
            }
        }

        if (step != null) {
            const numericStep = Number(step);
            const numericMax = Number(maxSteps);
            if (!Number.isFinite(numericStep)) return;
            if (
                !this.currentlyExecuting.step ||
                numericStep < Number(this.currentlyExecuting.step)
            ) {
                this.currentlyExecuting.pass += 1;
            }
            this.currentlyExecuting.step = numericStep;
            this.currentlyExecuting.maxSteps = Number.isFinite(numericMax) ? numericMax : null;
        }
    }

    error(details: any) {
        this.errorDetails = details || null;
    }
}

export class FloatingViewerProgressService extends EventTarget {
    [key: string]: any;
    constructor({
        getApi = (app: any) => getRawHostApi(app),
        getApp = () => getRawHostApp(),
        waitForApi = (options: any) => waitForRawHostApi(options),
    } = {}) {
        super();
        this._getApi = getApi;
        this._getApp = getApp;
        this._waitForApi = waitForApi;
        this.promptsMap = new Map();
        this.currentExecution = null;
        this.lastQueueRemaining = 0;
        this._api = null;
        this._listenerEntries = [];
        this._initPromise = null;
        this._queuePromptBinding = null;
    }

    getSnapshot() {
        return {
            queue: this.lastQueueRemaining,
            prompt: this.currentExecution,
        };
    }

    getCurrentNodeId() {
        const prompt = this.currentExecution;
        return String(
            prompt?.errorDetails?.node_id || prompt?.currentlyExecuting?.nodeId || "",
        ).trim();
    }

    getOrMakePrompt(id: any) {
        const safeId = String(id || "").trim() || "unknown";
        let prompt = this.promptsMap.get(safeId);
        if (!prompt) {
            prompt = new FloatingViewerPromptExecution(safeId, this._getApp);
            this.promptsMap.set(safeId, prompt);
        }
        return prompt;
    }

    async ensureInitialized({ api = null, app = null, timeoutMs = 0 } = {}) {
        if (api && this._api === api) return api;
        if (!api && this._api) return this._api;
        if (this._initPromise) return this._initPromise;

        this._initPromise = this._ensureInitializedInternal({ api, app, timeoutMs }).finally(() => {
            this._initPromise = null;
        });
        return this._initPromise;
    }

    async _ensureInitializedInternal({ api = null, app = null, timeoutMs = 0 } = {}) {
        let resolvedApi = api || this._getApi?.(app) || null;
        if (!resolvedApi && timeoutMs > 0 && typeof this._waitForApi === "function") {
            try {
                resolvedApi = await this._waitForApi({ app, timeoutMs });
            } catch (e: any) {
                console.debug?.(e);
            }
        }
        if (!resolvedApi) return null;
        this._attachApi(resolvedApi);
        return resolvedApi;
    }

    _attachApi(api: any) {
        if (!api || typeof api.addEventListener !== "function") return;
        if (this._api === api) return;
        this.dispose({ resetPatchedQueuePrompt: false, keepState: true });
        this._api = api;
        this._patchQueuePrompt(api);
        this._attachApiListeners(api);
    }

    _patchQueuePrompt(api: any) {
        const service = this;
        this._queuePromptBinding = wrapHostQueuePrompt({
            api,
            owner: this,
            createWrapper(originalQueuePrompt: any) {
                return async function (this: any, number: any, prompt: any, ...args: any[]) {
                    let response;
                    try {
                        response = await originalQueuePrompt.apply(this, [number, prompt, ...args]);
                    } catch (error: any) {
                        const promptExecution = service.getOrMakePrompt("error");
                        promptExecution.error({ exception_type: "Unknown." });
                        service.currentExecution = promptExecution;
                        service.dispatchProgressUpdate();
                        throw error;
                    }

                    const promptId = String(response?.prompt_id || response?.promptId || "").trim();
                    if (promptId) {
                        const promptExecution = service.getOrMakePrompt(promptId);
                        promptExecution.setPrompt(prompt);
                        if (!service.currentExecution) {
                            service.currentExecution = promptExecution;
                        }
                        service.dispatchEvent(
                            makeCustomEvent("queue-prompt", {
                                prompt: promptExecution,
                            }),
                        );
                        service.dispatchProgressUpdate();
                    }
                    return response;
                };
            },
        });
    }

    _attachApiListeners(api: any) {
        this._attachListener(api, "status", (event: any) => {
            if (!event?.detail?.exec_info) return;
            this.lastQueueRemaining = Number(event.detail.exec_info.queue_remaining) || 0;
            this.dispatchProgressUpdate();
        });

        this._attachListener(api, "execution_start", (event: any) => {
            const promptId = String(
                event?.detail?.prompt_id || event?.detail?.promptId || "",
            ).trim();
            if (!promptId) return;
            this.currentExecution = this.getOrMakePrompt(promptId);
            this.dispatchProgressUpdate();
        });

        this._attachListener(api, "executing", (event: any) => {
            if (!this.currentExecution) {
                this.currentExecution = this.getOrMakePrompt("unknown");
            }
            this.currentExecution.executing(event?.detail);
            if (event?.detail == null) {
                this.currentExecution = null;
            }
            this.dispatchProgressUpdate();
        });

        this._attachListener(api, "progress", (event: any) => {
            const detail = event?.detail || {};
            if (!this.currentExecution) {
                this.currentExecution = this.getOrMakePrompt(detail.prompt_id || detail.promptId);
            }
            this.currentExecution.executing(detail.node, detail.value, detail.max);
            this.dispatchProgressUpdate();
        });

        this._attachListener(api, "execution_cached", (event: any) => {
            const detail = event?.detail || {};
            if (!this.currentExecution) {
                this.currentExecution = this.getOrMakePrompt(detail.prompt_id || detail.promptId);
            }
            for (const cached of Array.isArray(detail.nodes) ? detail.nodes : []) {
                this.currentExecution.markExecuted(cached);
            }
            this.dispatchProgressUpdate();
        });

        this._attachListener(api, "executed", (event: any) => {
            const detail = event?.detail || {};
            if (!this.currentExecution) {
                const promptId = String(detail.prompt_id || detail.promptId || "").trim();
                if (promptId) {
                    this.currentExecution = this.getOrMakePrompt(promptId);
                }
            }
        });

        this._attachListener(api, "execution_error", (event: any) => {
            const detail = event?.detail || {};
            if (!this.currentExecution) {
                this.currentExecution = this.getOrMakePrompt(detail.prompt_id || detail.promptId);
            }
            this.currentExecution?.error(detail);
            this.dispatchProgressUpdate();
        });

        const clearCurrentExecution = () => {
            if (this.currentExecution) {
                this.currentExecution.executing(null);
            }
            this.currentExecution = null;
            this.dispatchProgressUpdate();
        };
        this._attachListener(api, "execution_success", clearCurrentExecution);
        this._attachListener(api, "execution_interrupted", clearCurrentExecution);
    }

    _attachListener(api: any, type: any, handler: any) {
        api.addEventListener(type, handler);
        this._listenerEntries.push({ api, type, handler });
    }

    dispatchProgressUpdate() {
        this.dispatchEvent(makeCustomEvent(PROGRESS_UPDATE_EVENT, this.getSnapshot()));
    }

    dispose({ resetPatchedQueuePrompt = false, keepState = false } = {}) {
        for (const { api, type, handler } of this._listenerEntries.splice(0)) {
            try {
                api?.removeEventListener?.(type, handler);
            } catch (e: any) {
                console.debug?.(e);
            }
        }

        if (resetPatchedQueuePrompt && this._queuePromptBinding?.owner === this) {
            try {
                this._queuePromptBinding.restore?.();
            } catch (e: any) {
                console.debug?.(e);
            }
        }

        this._queuePromptBinding = null;
        this._api = null;
        if (!keepState) {
            this.promptsMap.clear();
            this.currentExecution = null;
            this.lastQueueRemaining = 0;
        }
    }
}

const globalHost = getGlobalHost() as Record<string, any>;

export const floatingViewerProgressService =
    globalHost[GLOBAL_SERVICE_KEY] || new FloatingViewerProgressService();

if (!globalHost[GLOBAL_SERVICE_KEY]) {
    globalHost[GLOBAL_SERVICE_KEY] = floatingViewerProgressService;
}

export function ensureFloatingViewerProgressTracking(options: Record<string, any> = {}): Promise<any> {
    return floatingViewerProgressService.ensureInitialized(options);
}

function formatProgressText(detail: any) {
    const queue = Math.max(0, Number(detail?.queue) || 0);
    const prompt = detail?.prompt || null;

    if (prompt?.errorDetails) {
        const exceptionType = String(
            prompt.errorDetails?.exception_type || "Execution error",
        ).trim();
        const nodeId = String(prompt.errorDetails?.node_id || "").trim();
        const nodeType = String(prompt.errorDetails?.node_type || "").trim();
        return [exceptionType, nodeId, nodeType].filter(Boolean).join(" ");
    }

    if (prompt?.currentlyExecuting) {
        const current = prompt.currentlyExecuting;
        let progressText = `(${queue}) `;
        if (!prompt.totalNodes) {
            progressText += "??%";
        } else {
            const percent = (prompt.executedNodeIds.length / prompt.totalNodes) * 100;
            progressText += `${Math.round(percent)}%`;
        }

        let stepsLabel = "";
        if (current.step != null && current.maxSteps) {
            const stepPercent = (Number(current.step) / Number(current.maxSteps)) * 100;
            if (current.pass > 1 || current.maxPasses != null) {
                stepsLabel += `#${current.pass}`;
                if (current.maxPasses && current.maxPasses > 0) {
                    stepsLabel += `/${current.maxPasses}`;
                }
                stepsLabel += " - ";
            }
            stepsLabel += `${Math.round(stepPercent)}%`;
        }

        const nodeLabel = String(current.nodeLabel || "").trim();
        if (nodeLabel || stepsLabel) {
            progressText += ` - ${nodeLabel || "Unknown node"}${stepsLabel ? ` (${stepsLabel})` : ""}`;
        }
        return progressText;
    }

    if (queue > 0) {
        return `(${queue}) Running... in another tab`;
    }
    return "Idle";
}

export function formatFloatingViewerMediaProgressText(detail: any): string {
    const prompt = detail?.prompt || null;

    if (prompt?.errorDetails) {
        const errorDetails = prompt?.errorDetails || {};
        const nodeLabel = String(
            prompt?.currentlyExecuting?.nodeLabel ||
                errorDetails?.node_type ||
                errorDetails?.node_id ||
                "Execution",
        ).trim();
        const rawMessage =
            errorDetails?.exception_message ??
            errorDetails?.error ??
            errorDetails?.message ??
            errorDetails?.detail ??
            errorDetails?.reason ??
            "";
        const normalizedMessage = Array.isArray(rawMessage)
            ? rawMessage
                  .map((item: any) => String(item || "").trim())
                  .filter(Boolean)
                  .join(" | ")
            : String(rawMessage || "").trim();
        const compactMessage = normalizedMessage.replace(/\s+/g, " ").trim();
        if (compactMessage) {
            return `${nodeLabel} - ${compactMessage}`;
        }
        return `${nodeLabel} - Error`;
    }

    const current = prompt?.currentlyExecuting || null;
    if (!current) return "";

    const nodeLabel = String(current.nodeLabel || current.nodeId || "Node").trim();
    const step = Number(current.step);
    const maxSteps = Number(current.maxSteps);
    if (Number.isFinite(step) && Number.isFinite(maxSteps) && maxSteps > 0) {
        if (current.pass > 1) {
            return `${nodeLabel} #${current.pass} - ${step}/${maxSteps}`;
        }
        return `${nodeLabel} - ${step}/${maxSteps}`;
    }
    return nodeLabel;
}

function applyFloatingViewerProgressState(viewer: any, detail: any) {
    if (!viewer?._progressEl && !viewer?._mediaProgressEl) return;

    const prompt = detail?.prompt || null;
    const currentNodeId = String(
        prompt?.errorDetails?.node_id || prompt?.currentlyExecuting?.nodeId || "",
    ).trim();

    let nodesWidth = "0%";
    let stepsWidth = "0%";
    const isError = !!prompt?.errorDetails;

    if (prompt?.currentlyExecuting) {
        if (prompt.totalNodes > 0) {
            const percent = (prompt.executedNodeIds.length / prompt.totalNodes) * 100;
            nodesWidth = `${Math.max(2, Math.round(percent * 100) / 100)}%`;
        }

        const step = Number(prompt.currentlyExecuting?.step);
        const maxSteps = Number(prompt.currentlyExecuting?.maxSteps);
        if (Number.isFinite(step) && Number.isFinite(maxSteps) && maxSteps > 0) {
            stepsWidth = `${Math.max(0, Math.min(100, (step / maxSteps) * 100))}%`;
        }
    } else if (isError) {
        nodesWidth = "100%";
        stepsWidth = "100%";
    }

    viewer._progressCurrentNodeId = currentNodeId || null;
    if (viewer._progressEl) {
        viewer._progressNodesEl.style.width = nodesWidth;
        viewer._progressStepsEl.style.width = stepsWidth;
        viewer._progressTextEl.textContent = formatProgressText(detail);
        viewer._progressEl.classList.toggle("is-error", isError);
        viewer._progressEl.classList.toggle("is-clickable", Boolean(currentNodeId));
        viewer._progressEl.title = currentNodeId
            ? "Execution progress - click to center active node"
            : "Execution progress";
    }

    if (viewer._mediaProgressEl) {
        const mediaLabel = formatFloatingViewerMediaProgressText(detail);
        viewer._mediaProgressTextEl.textContent = mediaLabel;
        viewer._mediaProgressEl.title = mediaLabel || "";
        viewer._mediaProgressEl.classList.toggle("is-error", isError);
        viewer._mediaProgressEl.classList.toggle("is-visible", Boolean(mediaLabel));
    }
}

function centerFloatingViewerProgressNode(viewer: any, nodeId: any) {
    const safeNodeId = String(nodeId || "").trim();
    if (!safeNodeId) return false;
    try {
        return centerHostCanvasNodeById(safeNodeId, getRawHostApp());
    } catch (e: any) {
        console.debug?.(e);
        return false;
    }
}

export function buildFloatingViewerProgressBar(viewer: any): HTMLElement {
    const root = document.createElement("div");
    root.className = "mjr-mfv-progress";
    root.setAttribute("role", "status");
    root.setAttribute("aria-live", "polite");

    const nodesBar = document.createElement("div");
    nodesBar.className = "mjr-mfv-progress-bar mjr-mfv-progress-bar--nodes";

    const stepsBar = document.createElement("div");
    stepsBar.className = "mjr-mfv-progress-bar mjr-mfv-progress-bar--steps";

    const overlay = document.createElement("div");
    overlay.className = "mjr-mfv-progress-overlay";
    overlay.setAttribute("aria-hidden", "true");

    const text = document.createElement("span");
    text.className = "mjr-mfv-progress-text";
    text.textContent = "Idle";

    root.appendChild(nodesBar);
    root.appendChild(stepsBar);
    root.appendChild(overlay);
    root.appendChild(text);

    root.addEventListener("pointerdown", (event: any) => {
        if (event.button !== 0) return;
        if (!centerFloatingViewerProgressNode(viewer, viewer._progressCurrentNodeId)) return;
        event.preventDefault();
        event.stopPropagation();
    });

    viewer._progressEl = root;
    viewer._progressNodesEl = nodesBar;
    viewer._progressStepsEl = stepsBar;
    viewer._progressTextEl = text;

    if (viewer._progressUpdateHandler) {
        floatingViewerProgressService.removeEventListener(
            PROGRESS_UPDATE_EVENT,
            viewer._progressUpdateHandler,
        );
    }
    viewer._progressUpdateHandler = (event: any) => {
        applyFloatingViewerProgressState(
            viewer,
            event?.detail || floatingViewerProgressService.getSnapshot(),
        );
    };

    floatingViewerProgressService.addEventListener(
        PROGRESS_UPDATE_EVENT,
        viewer._progressUpdateHandler,
    );
    void ensureFloatingViewerProgressTracking({ timeoutMs: 4000 }).catch((e: any) => {
        console.debug?.(e);
    });
    applyFloatingViewerProgressState(viewer, floatingViewerProgressService.getSnapshot());

    return root;
}

export function buildFloatingViewerMediaProgressOverlay(viewer: any) {
    const root = document.createElement("div");
    root.className = "mjr-mfv-media-progress";
    root.setAttribute("aria-hidden", "true");

    const text = document.createElement("span");
    text.className = "mjr-mfv-media-progress-text";
    root.appendChild(text);

    viewer._mediaProgressEl = root;
    viewer._mediaProgressTextEl = text;
    applyFloatingViewerProgressState(viewer, floatingViewerProgressService.getSnapshot());
    return root;
}

export function disposeFloatingViewerProgressBar(viewer: any) {
    if (viewer?._progressUpdateHandler) {
        try {
            floatingViewerProgressService.removeEventListener(
                PROGRESS_UPDATE_EVENT,
                viewer._progressUpdateHandler,
            );
        } catch (e: any) {
            console.debug?.(e);
        }
    }
    viewer._progressUpdateHandler = null;
    viewer._progressCurrentNodeId = null;
    viewer._progressEl = null;
    viewer._progressNodesEl = null;
    viewer._progressStepsEl = null;
    viewer._progressTextEl = null;
    viewer._mediaProgressEl = null;
    viewer._mediaProgressTextEl = null;
}
