/**
 * WorkflowNodesTab lists all workflow nodes and exposes each node's parameters
 * inline through a collapsible section.
 *
 * Subgraph container nodes show an extra toggle to expand/collapse their inner
 * nodes as a nested group below the container.
 *
 * The node list DOM is only rebuilt when the set of nodes actually changes —
 * value sync runs every frame without touching the DOM structure.
 */

import { focusHostCanvasNode, getHostGraph, getSelectedHostCanvasNodeIds } from "../../../app/hostAdapter.js";
import { getGraphNodes, getNodeSubgraphs } from "../../../app/graphTraversal.js";
import { NodeWidgetRenderer } from "./NodeWidgetRenderer.js";

type LooseRecord = Record<string, any>;
type WorkflowTreeNode = { node: LooseRecord; children: WorkflowTreeNode[] };
type WorkflowTreeItemElement = HTMLDivElement & {
    _mjrNodeId?: string;
    _mjrParentTreeItem?: WorkflowTreeItemElement | null;
    _mjrChildrenToggle?: HTMLButtonElement;
    _mjrChildrenEl?: HTMLDivElement;
    _mjrChildCount?: number;
};

export class WorkflowNodesTab {
    [key: string]: any;
    _searchQuery: string;
    _expandedNodeIds: Set<string>;
    _expandedChildrenIds: Set<string>;
    _renderers: NodeWidgetRenderer[];
    _el: HTMLElement;
    _searchInput!: HTMLInputElement;
    _list!: HTMLDivElement;
    _lastNodeSig: string;
    _lastSelectedId: string;

    constructor() {
        this._searchQuery = "";
        /** Which node widget panels are open (at most one at a time). */
        this._expandedNodeIds = new Set();
        /** Which subgraph container nodes have their children list open. */
        this._expandedChildrenIds = new Set();
        /** Flat list of every NodeWidgetRenderer currently in the DOM. */
        this._renderers = [];
        this._el = this._build();
        this._lastNodeSig = "";
        this._lastSelectedId = "";
    }

    get el() {
        return this._el;
    }

    refresh() {
        this._maybeRebuildList();
        for (const renderer of this._renderers) renderer.syncFromGraph();
    }

    forceRebuild() {
        this._lastNodeSig = "";
        this._maybeRebuildList();
    }

    dispose() {
        for (const renderer of this._renderers) renderer.dispose();
        this._renderers = [];
        this._el?.remove?.();
    }

    _build() {
        const wrap = document.createElement("div");
        wrap.className = "mjr-ws-nodes-tab";

        const searchWrap = document.createElement("div");
        searchWrap.className = "mjr-ws-search-wrap";

        const searchIcon = document.createElement("i");
        searchIcon.className = "pi pi-search mjr-ws-search-icon";
        searchIcon.setAttribute("aria-hidden", "true");
        searchWrap.appendChild(searchIcon);

        this._searchInput = document.createElement("input");
        this._searchInput.type = "text";
        this._searchInput.className = "mjr-ws-search";
        this._searchInput.placeholder = "Search nodes...";
        this._searchInput.addEventListener("input", () => {
            this._searchQuery = this._searchInput.value;
            this.forceRebuild();
        });
        searchWrap.appendChild(this._searchInput);

        const clearBtn = document.createElement("button");
        clearBtn.type = "button";
        clearBtn.className = "mjr-ws-search-clear";
        clearBtn.title = "Clear search";
        clearBtn.innerHTML = '<i class="pi pi-times" aria-hidden="true"></i>';
        clearBtn.addEventListener("click", () => {
            this._searchInput.value = "";
            this._searchQuery = "";
            this.forceRebuild();
        });
        searchWrap.appendChild(clearBtn);

        wrap.appendChild(searchWrap);

        this._list = document.createElement("div");
        this._list.className = "mjr-ws-nodes-list";
        wrap.appendChild(this._list);

        return wrap;
    }

    _syncCanvasSelection() {
        const selectedIds = _getSelectedNodeIds();
        const selectedId = selectedIds[0] || "";
        // Toggle the highlight class on every render so it survives DOM
        // rebuilds (signature changes after adding/removing a node).
        let target: any = null;
        for (const renderer of this._renderers) {
            const isSelected = String(renderer._node?.id ?? "") === selectedId;
            renderer.el?.classList?.toggle("is-selected-from-graph", isSelected);
            if (isSelected) target = renderer;
        }
        if (!selectedId) {
            this._lastSelectedId = "";
            return;
        }
        if (selectedId === this._lastSelectedId) return;
        this._lastSelectedId = selectedId;
        if (!target) return;
        if (!target._expanded) target.setExpanded(true);
        // Promote the selected node to the first position of its container.
        // For nested nodes, that's the top of their parent subgraph group;
        // top-level nodes go to the very top of the sidebar list. Order
        // resets naturally on the next full rebuild (graph topology change).
        try {
            const itemEl = target._mjrTreeItemEl || target.el;
            this._openTreeBranch(itemEl);
            const parent = itemEl?.parentElement;
            if (parent && parent.firstElementChild !== itemEl) {
                parent.insertBefore(itemEl, parent.firstElementChild);
            }
            target.el?.scrollIntoView({ block: "start", inline: "nearest" });
        } catch (e: any) {
            console.debug?.("[MFV] promote selected node failed", e);
        }
    }

    _maybeRebuildList() {
        const tree = _buildNodeTree(_getGraph());
        const q = (this._searchQuery || "").toLowerCase().trim();
        const filtered = q ? _filterTree(tree, q) : tree;
        const sig = _treeSignature(filtered);

        if (sig === this._lastNodeSig) {
            this._syncCanvasSelection();
            return;
        }
        this._lastNodeSig = sig;

        for (const renderer of this._renderers) renderer.dispose();
        this._renderers = [];
        this._list.innerHTML = "";

        if (!filtered.length) {
            const empty = document.createElement("div");
            empty.className = "mjr-ws-sidebar-empty";
            empty.textContent = tree.length ? "No nodes match your search" : "No nodes in workflow";
            this._list.appendChild(empty);
            return;
        }

        this._renderItems(filtered, this._list, 0, null);
        this._syncCanvasSelection();
    }

    /**
     * Recursively render tree items into a container.
     * @param {{node: object, children: object[]}[]} items
     * @param {HTMLElement} container
     * @param {number} depth
     */
    _renderItems(
        items: WorkflowTreeNode[],
        container: HTMLElement,
        depth: number,
        parentItem: WorkflowTreeItemElement | null,
    ) {
        for (const { node, children } of items) {
            const nodeId = String(node?.id ?? "");
            const childCount = children.length;

            const itemEl: WorkflowTreeItemElement = document.createElement("div");
            itemEl.className = "mjr-ws-tree-item";
            itemEl.dataset.nodeId = nodeId;
            itemEl._mjrNodeId = nodeId;
            itemEl._mjrParentTreeItem = parentItem || null;
            if (childCount > 0) itemEl.classList.add("mjr-ws-tree-item--subgraph");
            if (depth > 0) itemEl.classList.add("mjr-ws-tree-item--nested");

            const renderer = new NodeWidgetRenderer(node, {
                collapsible: true,
                expanded: this._expandedNodeIds.has(nodeId),
                depth,
                isSubgraph: childCount > 0,
                childCount,
                onLocate: () => _focusNode(node),
                onToggle: (expanded) => {
                    if (expanded) {
                        this._expandedNodeIds = new Set([nodeId]);
                        for (const r of this._renderers) {
                            if (r !== renderer) r.setExpanded(false);
                        }
                    } else {
                        this._expandedNodeIds.delete(nodeId);
                    }
                },
            });
            renderer._mjrTreeItemEl = itemEl;
            this._renderers.push(renderer);
            itemEl.appendChild(renderer.el);

            if (childCount > 0) {
                const isOpen = this._expandedChildrenIds.has(nodeId);

                const toggleBtn = document.createElement("button");
                toggleBtn.type = "button";
                toggleBtn.className = "mjr-ws-children-toggle";
                if (depth > 0) toggleBtn.classList.add("mjr-ws-children-toggle--nested");
                _setChildrenToggleLabel(toggleBtn, childCount, isOpen);

                const childrenEl = document.createElement("div");
                childrenEl.className = "mjr-ws-children";
                childrenEl.hidden = !isOpen;
                itemEl._mjrChildrenToggle = toggleBtn;
                itemEl._mjrChildrenEl = childrenEl;
                itemEl._mjrChildCount = childCount;

                this._renderItems(children, childrenEl, depth + 1, itemEl);

                toggleBtn.addEventListener("click", () => {
                    this._setTreeItemChildrenOpen(itemEl, childrenEl.hidden);
                });

                itemEl.appendChild(toggleBtn);
                itemEl.appendChild(childrenEl);
            }

            container.appendChild(itemEl);
        }
    }

    _setTreeItemChildrenOpen(itemEl: any, open: any) {
        if (!itemEl?._mjrChildrenEl || !itemEl?._mjrChildrenToggle) return;
        const nodeId = String(itemEl._mjrNodeId || "");
        itemEl._mjrChildrenEl.hidden = !open;
        if (nodeId) {
            if (open) this._expandedChildrenIds.add(nodeId);
            else this._expandedChildrenIds.delete(nodeId);
        }
        _setChildrenToggleLabel(
            itemEl._mjrChildrenToggle,
            Number(itemEl._mjrChildCount) || 0,
            open,
        );
    }

    _openTreeBranch(itemEl: any) {
        let current = itemEl || null;
        while (current) {
            const parent = current._mjrParentTreeItem || null;
            if (parent) this._setTreeItemChildrenOpen(parent, true);
            current = parent;
        }
        this._setTreeItemChildrenOpen(itemEl, true);
    }
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function _getGraph() {
    try {
        return getHostGraph();
    } catch {
        return null;
    }
}

function _getSubgraphsFromNode(node: any) {
    return getNodeSubgraphs(node);
}

/**
 * Build a recursive tree: [{node, children}] from a LiteGraph graph object.
 */
function _buildNodeTree(graph: any, visited = new Set()): any[] {
    if (!graph || visited.has(graph)) return [];
    visited.add(graph);
    const nodes = getGraphNodes(graph);
    const result = [];
    for (const node of nodes) {
        if (!node) continue;
        const subgraphs = _getSubgraphsFromNode(node);
        const children = subgraphs.flatMap((sg: any) => _buildNodeTree(sg, visited));
        result.push({ node, children });
    }
    return result;
}

/**
 * Recursively filter tree items. A container is kept if it matches OR any child matches.
 */
function _filterTree(items: any, q: any): any[] {
    const result = [];
    for (const { node, children } of items) {
        const nodeMatches =
            (node.type || "").toLowerCase().includes(q) ||
            (node.title || "").toLowerCase().includes(q);
        const filteredChildren = _filterTree(children, q);
        if (nodeMatches || filteredChildren.length > 0) {
            result.push({ node, children: filteredChildren });
        }
    }
    return result;
}

/** Depth-first signature of all node IDs — encodes both set and tree order. */
function _treeSignature(items: any) {
    const ids: any[] = [];
    function collect(arr: any) {
        for (const { node, children } of arr) {
            ids.push(node.id);
            ids.push("[");
            collect(children);
            ids.push("]");
        }
    }
    collect(items);
    return ids.join(",");
}

function _setChildrenToggleLabel(btn: any, count: any, isOpen: any) {
    const icon = isOpen ? "pi-chevron-down" : "pi-chevron-right";
    btn.textContent = "";
    const iconEl = document.createElement("i");
    iconEl.className = `pi ${icon}`;
    iconEl.setAttribute("aria-hidden", "true");
    btn.appendChild(iconEl);
    const label = document.createElement("span");
    label.textContent = `${count} inner node${count !== 1 ? "s" : ""}`;
    btn.appendChild(label);
    btn.setAttribute("aria-expanded", String(isOpen));
}

function _focusNode(node: any) {
    try {
        focusHostCanvasNode(node);
    } catch (e: any) {
        console.debug?.("[MFV] _focusNode", e);
    }
}

function _getSelectedNodeIds() {
    try {
        return getSelectedHostCanvasNodeIds();
    } catch (e: any) {
        console.debug?.("[MFV] _getSelectedNodeIds", e);
    }
    return [];
}
