class KnowledgeMindmapGraph {
    constructor(options = {}) {
        this.graph = null;
        this.rawTree = null;
        this.treeData = null;
        this.relationEdges = [];
        this.masteryData = {};
        this.masteryCache = {};
        this.stats = { nodes: 0, extraEdges: 0 };
        this.containerId = options.containerId || 'knowledge-mindmap';
        this.livewireMethod = options.livewireMethod || 'openDrawer';
        this.onNodeSelect = options.onNodeSelect || null;
        this.livewireId = options.livewireId || null;
        this.highlightLowMastery = options.highlightLowMastery ?? true;
        this.emitSelection = options.emitSelection ?? true;
        this.tooltipEl = null;
        this.nodeIdSet = new Set();
        this.lockRules = options.lockRules || [
            { prerequisite: 'P04', target: 'P05', threshold: 0.6 },
            { prerequisite: 'P05', target: 'P06', threshold: 0.6 },
        ];
        this.parentMap = {};
        this.clickTimer = null;
        this.clickDelay = 220;
        this.focusListener = null;
        this.showEdges = options.showEdges ?? true;
        this.showRelationEdges = options.showRelationEdges ?? true;

        this.setMasteryData(options.masteryData || {});
    }

    async init() {
        try {
            await this.loadData();
            this.applyUnlockRules(this.treeData);
            this.applyInitialCollapse(this.treeData);
            this.expandForMastery();
            this.renderGraph();
            this.bindEvents();
            this.setupLivewireListeners();
            window.addEventListener('resize', () => this.resizeGraph());
        } catch (error) {
            console.error('初始化思维导图失败', error);
        }
    }

    async loadData() {
        const [treeResp, edgesResp] = await Promise.all([
            fetch('/data/tree.json'),
            fetch('/data/edges.json'),
        ]);

        this.rawTree = await treeResp.json();
        const edges = await edgesResp.json();
        const rawEdges = Array.isArray(edges) ? edges : edges?.edges || [];

        this.masteryCache = {};
        this.treeData = this.transformNode(this.rawTree);
        this.buildParentMap(this.treeData);
        this.relationEdges = this.normalizeEdges(rawEdges);
        const flatIds = [];
        this.collectIds(this.treeData, flatIds);
        console.log('知识点总数', flatIds.length, '列表:', flatIds);
        this.nodeIdSet = new Set(flatIds);
        this.logMasteryCoverage();
        this.stats = {
            nodes: this.countNodes(this.treeData),
            extraEdges: this.showRelationEdges ? this.relationEdges.length : 0,
        };
    }

    transformNode(node, depth = 0) {
        if (!node) return null;

        const id =
            node.code ||
            node.id ||
            node.label ||
            `node-${Math.random().toString(36).slice(2, 8)}`;
        const label = node.name || node.label || node.code || node.id || id;
        const masteryInfo = this.masteryData[id] || null;
        const masteryLevel = this.getMasteryLevel(id);
        const accuracy = this.toNumber(masteryInfo?.accuracy_rate);
        const recommended = masteryLevel < 0.6;

        const model = {
            id,
            label: `${id} · ${label}`,
            depth,
            locked: false,
            collapsed: depth > 0 && (node.children || []).length > 0, // 默认折叠所有有子节点的节点（除根节点）
            meta: {
                code: id,
                name: label,
                mastery_level: masteryLevel,
                accuracy_rate: accuracy,
                total_attempts: this.toNumber(masteryInfo?.total_attempts),
                mastery_info: masteryInfo,
                has_mastery: Boolean(masteryInfo),
                recommended,
            },
            children: (node.children || [])
                .map((child) => this.transformNode(child, depth + 1))
                .filter(Boolean),
        };

        return model;
    }

    getMasteryLevel(id) {
        if (this.masteryCache[id] !== undefined) {
            return this.masteryCache[id];
        }

        const remote = this.masteryData[id]?.mastery_level;
        const value = this.normalizeMasteryLevel(remote);

        this.masteryCache[id] = value;
        return value;
    }

    applyUnlockRules(node) {
        if (!node) return;

        const rule = this.lockRules.find((item) => item.target === node.id);
        const prereqMastery = rule
            ? this.masteryCache[rule.prerequisite] ?? 0
            : 1;

        const lockedByRule = rule ? prereqMastery < rule.threshold : false;

        node.locked = lockedByRule;
        if (node.locked) {
            node.meta.lock_reason = lockedByRule
                ? `需先掌握前置知识点：${rule.prerequisite}`
                : '需先掌握前置知识点';
        }

        node.children.forEach((child) => this.applyUnlockRules(child));
    }

    applyInitialCollapse(node, depth = 0) {
        if (!node) return;
        // 默认折叠所有有子节点的节点（除了根节点）
        if (depth > 0 && node.children.length > 0) {
            node.collapsed = true;
        }
        node.children.forEach((child) =>
            this.applyInitialCollapse(child, depth + 1)
        );
    }

    expandForMastery() {
        if (!this.treeData) return;
        const masteryKeys = new Set(Object.keys(this.masteryData || {}));
        if (!masteryKeys.size) return;
        this.expandNodesForMastery(this.treeData, masteryKeys);
    }

    expandNodesForMastery(node, masteryKeys) {
        if (!node) return false;
        const hasMastery = masteryKeys.has(node.id);
        let childHas = false;
        (node.children || []).forEach((child) => {
            if (this.expandNodesForMastery(child, masteryKeys)) {
                childHas = true;
            }
        });

        if (hasMastery || childHas) {
            node.collapsed = false;
        }

        return hasMastery || childHas;
    }

    countNodes(node) {
        if (!node) return 0;
        return (
            1 +
            node.children.reduce(
                (sum, child) => sum + this.countNodes(child),
                0
            )
        );
    }

    normalizeEdges(rawEdges) {
        const seen = new Set();
        const normalized = [];
        const styleMap = {
            prerequisite: { stroke: '#8C8986FF', lineDash: [8, 6], lineWidth: 1, label: '前置' },
            successor: { stroke: '#8C8986FF', lineDash: [8, 6], lineWidth: 1, label: '后继' },
            crosslink: { stroke: '#8c8a89', lineDash: [8, 6], lineWidth: 1, label: '跨联' },
            sibling: { stroke: '#8C8986FF', lineDash: [8, 6], lineWidth: 1, label: '同级' },
        };

        (rawEdges || []).forEach((edge, index) => {
            if (!edge?.source || !edge?.target) return;

            const key = `${edge.source}-${edge.target}-${edge.type}`;
            if (seen.has(key)) return;
            seen.add(key);

            const category = edge.type || 'successor';
            const renderType =
                category === 'successor' ? 'cubic-horizontal' : 'quadratic';
            const baseStyle = styleMap[category] || {
                stroke: '#cbd5e1',
                lineWidth: 2.5,
            };
            const label = baseStyle.label || edge.label || category;
            const arrowStroke = baseStyle.stroke || '#cbd5e1';
            const style = {
                ...baseStyle,
                endArrow: {
                    path: 'M 0,0 L 8,4 L 0,8 z',
                    fill: arrowStroke,
                    d: 8,
                },
                startArrow: false,
            };

            normalized.push({
                id: `rel-${index}`,
                source: edge.source,
                target: edge.target,
                type: renderType,
                edgeType: category,
                style: {
                    ...style,
                },
                label,
                comment: edge.comment || edge.note || '',
            });
        });

        return normalized;
    }

    collectIds(node, bucket) {
        if (!node) return;
        bucket.push(node.id);
        (node.children || []).forEach((child) => this.collectIds(child, bucket));
    }

    renderGraph() {
        const container = document.getElementById(this.containerId);
        if (!container) return;

        const bounds = container.getBoundingClientRect();
        const width = Math.max(bounds.width, 640);
        const height = Math.max(bounds.height, 640);

        // 直接在数据层面设置折叠状态
        this.setCollapsedState(this.treeData);

        this.graph = new G6.TreeGraph({
            container: this.containerId,
            width,
            height,
            modes: {
                default: [
                    'drag-canvas',
                    'zoom-canvas',
                ],
            },
            defaultNode: {
                type: 'hexagon-card',
                size: 110,
            },
            defaultEdge: {
                type: 'cubic-horizontal',
                style: this.showEdges
                    ? {
                          stroke: '#cbd5e1',
                          lineWidth: 2,
                      }
                    : {
                          stroke: 'transparent',
                          lineWidth: 0,
                          opacity: 0,
                      },
            },
            nodeStateStyles: {
                hover: { shadowColor: '#38bdf8', shadowBlur: 24 },
                selected: { shadowColor: '#fb923c', shadowBlur: 28 },
                dimmed: { opacity: 0.3 },
                weak: { opacity: 0.6 },
                locked: { opacity: 0.35, cursor: 'not-allowed' },
            },
            edgeStateStyles: {
                hover: { lineWidth: 3, stroke: '#38bdf8' },
                connected: { opacity: 0.95, lineWidth: 3 },
                dimmed: { opacity: 0.25 },
                crosshover: { stroke: '#fb923c', lineWidth: 3 },
                glow: { shadowColor: '#facc15', shadowBlur: 12 },
            },
            layout: {
                type: 'mindmap',
                direction: 'H',
                getHeight: () => 110,
                getWidth: () => 150,
                getVGap: () => 18,
                getHGap: () => 50,
                preventOverlap: true,
            },
        });

        this.graph.data(this.treeData);
        this.graph.render();
        this.graph.fitView(12);
        // 暴露实例便于调试
        window.KnowledgeMindmapGraphInstance = this;
        window.KnowledgeMindmapG6Graph = this.graph;
        window.focusMindmapNode = (id) => this.focusNodeById(id);

        // 边提示
        if (this.showEdges) {
            this.bindEdgeTooltip();
        }

        this.applyNodeStates();
        if (this.showEdges) {
            this.startEdgeFlows();
        }
        this.focusOnLowestMastery();
        this.repaintNodes();
        if (!this.showEdges) {
            this.hideAllEdges();
        }

        // 折叠/展开或重新布局后重新挂载关联线
        this.graph.on('afterlayout', () => {
            this.redrawRelationEdges();
        });

        this.setupFocusListener();
    }

    setCollapsedState(nodeData, depth = 0) {
        if (!nodeData) return;

        // 折叠除根节点外的所有有子节点的节点
        if (depth > 0 && nodeData.children && nodeData.children.length > 0) {
            if (nodeData.collapsed === undefined) {
                nodeData.collapsed = true;
            }
        }

        // 递归处理子节点
        if (nodeData.children) {
            nodeData.children.forEach(child => this.setCollapsedState(child, depth + 1));
        }
    }

    clearRelationEdges() {
        if (!this.graph) return;
        this.graph.getEdges().forEach((edge) => {
            const id = edge.getModel()?.id || '';
            if (id.startsWith('rel-')) {
                this.graph.removeItem(edge);
            }
        });
    }

    drawRelationEdges() {
        if (
            !this.graph ||
            !this.relationEdges.length ||
            !this.showRelationEdges ||
            !this.showEdges
        )
            return;
        this.relationEdges.forEach((edge) => {
            // 尝试将隐藏节点映射到可见的父节点，避免关联线丢失
            let sourceId = edge.source;
            let targetId = edge.target;
            const resolveVisible = (id) => {
                let cur = id;
                while (cur) {
                    if (this.graph.findById(cur)) return cur;
                    cur = this.parentMap[cur];
                }
                return null;
            };
            if (!this.graph.findById(sourceId)) {
                const fallback = resolveVisible(sourceId);
                if (fallback) sourceId = fallback;
            }
            if (!this.graph.findById(targetId)) {
                const fallback = resolveVisible(targetId);
                if (fallback) targetId = fallback;
            }
            const sourceVisible = this.graph.findById(sourceId);
            const targetVisible = this.graph.findById(targetId);
            // 若两端都找不到可见节点，直接跳过
            if (!sourceVisible && !targetVisible) {
                return;
            }
            // 若只有一端可见，则将不可见端提升到其最近的可见父节点（若仍不可见则跳过）
            if (!sourceVisible) {
                const resolved = resolveVisible(edge.source);
                if (!resolved || !this.graph.findById(resolved)) return;
                sourceId = resolved;
            }
            if (!targetVisible) {
                const resolved = resolveVisible(edge.target);
                if (!resolved || !this.graph.findById(resolved)) return;
                targetId = resolved;
            }
            this.graph.addItem('edge', { ...edge, source: sourceId, target: targetId });
        });
    }

    applyNodeStates() {
        if (!this.graph) return;
        this.graph.getNodes().forEach((node) => {
            const model = node.getModel();
            const mastery = model.meta?.mastery_level ?? 0;
            const hasMastery =
                model.meta?.has_mastery ?? Boolean(this.masteryData[model.id]);
            this.graph.setItemState(node, 'dimmed', !hasMastery);
            if (model.locked) {
                this.graph.setItemState(node, 'locked', true);
            }
            if (hasMastery && mastery < 0.4 && this.highlightLowMastery) {
                this.graph.setItemState(node, 'weak', true);
            }
            if (hasMastery && mastery >= 0.8 && !model.locked) {
                this.playHalo(node);
            }
        });
    }

    startEdgeFlows() {
        if (!this.graph || !this.showEdges) return;
        const nodeMap = new Map(
            this.graph
                .getNodes()
                .map((node) => [node.getModel().id, node.getModel()])
        );
        this.graph.getEdges().forEach((edge) => {
            const model = edge.getModel();
            const sourceMastery =
                nodeMap.get(model.source)?.meta?.mastery_level ?? 0;
            const category = model.edgeType || model.type;
            if (sourceMastery >= 0.8 && category !== 'crosslink') {
                this.animateEdgeFlow(edge, '#facc15');
            }
        });
    }

    animateEdgeFlow(edge, color) {
        if (!edge || edge.__flowing) return;
        const keyShape = edge.getKeyShape?.();
        if (!keyShape || !keyShape.getTotalLength) return;

        const totalLength = keyShape.getTotalLength();
        keyShape.attr('lineDash', [20, 12]);
        keyShape.attr('stroke', color);
        edge.__flowing = true;

        keyShape.animate(
            (ratio) => ({
                lineDashOffset: -ratio * totalLength,
                opacity: 0.8 + 0.2 * Math.sin(ratio * Math.PI),
            }),
            { duration: 1600, repeat: true }
        );
    }

    playHalo(node) {
        const group = node.getContainer();
        const keyShape = node.getKeyShape();
        if (!group || !keyShape) return;

        const bbox = keyShape.getBBox();
        const halo = group.addShape('circle', {
            attrs: {
                x: bbox.centerX,
                y: bbox.centerY,
                r: Math.max(bbox.width, bbox.height) * 0.65,
                stroke: '#facc15',
                lineWidth: 2,
                opacity: 0.4,
            },
            name: 'halo-shape',
        });

        halo.animate(
            (ratio) => ({
                r: halo.attr('r') + ratio * 16,
                opacity: 0.4 - ratio * 0.4,
            }),
            {
                duration: 1200,
                easing: 'easeCubic',
                repeat: false,
                removeOnEnd: true,
            }
        );
    }

    bindEvents() {
        if (!this.graph) return;

        this.graph.on('node:mouseover', (evt) => {
            const item = evt.item;
            // if (!item || item.getModel().locked) return;
            this.graph.setItemState(item, 'hover', true);
            this.highlightNeighbors(item.getModel().id);
            this.showTooltip(evt, item.getModel());
        });

        this.graph.on('node:mouseout', (evt) => {
            const item = evt.item;
            if (!item) return;
            this.graph.setItemState(item, 'hover', false);
            this.clearNeighborHighlight();
            this.hideTooltip();
        });

        this.graph.on('node:click', (evt) => this.handleNodeClick(evt));

        // 双击用于折叠/展开
        this.graph.on('node:dblclick', (evt) => {
            if (this.clickTimer) {
                clearTimeout(this.clickTimer);
                this.clickTimer = null;
            }
            const model = evt.item?.getModel();
            if (!model) return;
            if (model.children && model.children.length > 0) {
                const nextState = !model.collapsed;
                this.graph.updateItem(evt.item, { collapsed: nextState });
                this.graph.layout?.();
                this.redrawRelationEdges();
            }
        });

        this.graph.on('canvas:click', () => {
            this.graph.getNodes().forEach((node) => {
                this.graph.clearItemStates(node);
            });
            this.clearNeighborHighlight();
            this.hideTooltip();
        });
    }

    flashNode(item) {
        const keyShape = item?.getKeyShape?.();
        if (!keyShape) return;
        keyShape.animate({ opacity: 0.8 }, { duration: 80 });
        keyShape.animate({ opacity: 1 }, { duration: 200, delay: 80 });
    }

    highlightNeighbors(nodeId) {
        if (!this.showEdges) return;
        const connected = new Set([nodeId]);
        this.graph.getEdges().forEach((edge) => {
            const model = edge.getModel();
            const related =
                model.source === nodeId || model.target === nodeId;
            this.graph.setItemState(edge, 'hover', related);
            const category = model.edgeType || model.type;
            if (category === 'crosslink') {
                this.graph.setItemState(edge, 'crosshover', related);
            }
            if (related) {
                connected.add(model.source);
                connected.add(model.target);
            } else {
                this.graph.clearItemStates(edge, ['hover', 'crosshover']);
            }
        });

        this.graph.getNodes().forEach((node) => {
            const id = node.getModel().id;
            this.graph.setItemState(node, 'dimmed', !connected.has(id));
        });
    }

    clearNeighborHighlight() {
        if (!this.showEdges) return;
        this.graph.getEdges().forEach((edge) => {
            this.graph.clearItemStates(edge, ['hover', 'crosshover']);
        });
        this.graph.getNodes().forEach((node) => {
            this.graph.setItemState(node, 'dimmed', false);
        });
    }

    notifySelection(model) {
        // 派发全局事件，便于外层（Alpine/Livewire）捕获
        try {
            const event = new CustomEvent('mindmap-node-selected', {
                detail: model,
            });
            window.dispatchEvent(event);
        } catch (e) {
            // ignore dispatch errors
        }

        if (typeof this.onNodeSelect === 'function') {
            this.onNodeSelect(model);
            return;
        }

        if (!window.Livewire) return;

        const targetId =
            this.livewireId ||
            document
                .querySelector('[data-knowledge-mindmap-root] [wire\\:id], [wire\\:id]')
                ?.getAttribute('wire:id');

        const component = targetId ? window.Livewire.find(targetId) : null;
        if (component?.call) {
            component.call(this.livewireMethod, model.id);
        }
    }

    ensureTooltipEl() {
        if (this.tooltipEl) return this.tooltipEl;
        const div = document.createElement('div');
        div.style.position = 'fixed';
        div.style.zIndex = '9999';
        div.style.pointerEvents = 'none';
        div.style.padding = '10px 12px';
        div.style.background = 'rgba(15,23,42,0.95)';
        div.style.color = '#e2e8f0';
        div.style.borderRadius = '10px';
        div.style.boxShadow = '0 10px 30px rgba(0,0,0,0.18)';
        div.style.fontSize = '12px';
        div.style.lineHeight = '1.4';
        document.body.appendChild(div);
        this.tooltipEl = div;
        return div;
    }

    showTooltip(evt, model) {
        const tip = this.ensureTooltipEl();
        const mastery = (model.meta?.mastery_level ?? 0) * 100;
        const attempts = model.meta?.total_attempts ?? 0;
        const recommended = model.meta?.recommended ? '是' : '否';
        const locked = model.locked ? '是' : '否';
        tip.innerHTML = `
            <div style="font-weight:700;font-size:13px;">${model.id} · ${model.meta?.name || model.label}</div>
            <div>掌握度：${mastery.toFixed(1)}%</div>
            <div>推荐练习：${recommended}</div>
            <div>尝试次数：${attempts}</div>
            <div>锁定：${locked}</div>
        `;
        const x =
            evt?.clientX ??
            evt?.canvasX ??
            evt?.x ??
            evt?.event?.clientX ??
            0;
        const y =
            evt?.clientY ??
            evt?.canvasY ??
            evt?.y ??
            evt?.event?.clientY ??
            0;
        tip.style.left = `${x + 16}px`;
        tip.style.top = `${y + 12}px`;
        tip.style.opacity = '1';
    }

    hideTooltip() {
        if (!this.tooltipEl) return;
        this.tooltipEl.style.opacity = '0';
    }

    setupLivewireListeners() {
        ['mastery-updated', 'mindmap-mastery-updated'].forEach((event) => {
            window.addEventListener(event, (detailEvent) => {
                const payload =
                    detailEvent.detail?.data ??
                    detailEvent.detail ??
                    {};
                this.setMasteryData(payload);
                this.refreshGraph();
            });
        });
    }

    focusOnLowestMastery() {
        if (!this.graph) return;

        const entries = Object.entries(this.masteryData || {}).filter(
            ([, value]) =>
                value && typeof value.mastery_level === 'number'
        );
        if (!entries.length) return;

        let targetId = null;
        let minLevel = Infinity;
        entries.forEach(([id, value]) => {
            const level = value.mastery_level;
            if (typeof level === 'number' && level < minLevel) {
                minLevel = level;
                targetId = id;
            }
        });

        if (!targetId) return;

        this.graph.getNodes().forEach((node) => {
            this.graph.clearItemStates(node);
        });

        const item = this.graph.findById(targetId);
        if (item) {
            this.graph.zoom(2);
            this.graph.focusItem(item, true, {
                easing: 'easeCubic',
                duration: 500,
            });
            this.graph.setItemState(item, 'selected', true);
        }
    }

    refreshGraph() {
        if (!this.graph || !this.rawTree) return;

        this.masteryCache = {};
        this.treeData = this.transformNode(this.rawTree);
        this.buildParentMap(this.treeData);
        const flatIds = [];
        this.collectIds(this.treeData, flatIds);
        this.nodeIdSet = new Set(flatIds);
        this.logMasteryCoverage();
        this.applyUnlockRules(this.treeData);
        this.applyInitialCollapse(this.treeData);
        this.expandForMastery();

        // 强制全量重绘，确保 meta/颜色更新
        this.graph.clear();
        this.graph.data(this.treeData);
        this.graph.render();
        this.repaintNodes();
        this.graph.fitView(12);
        // 暴露实例便于调试（刷新后仍可用）
        window.KnowledgeMindmapGraphInstance = this;
        window.KnowledgeMindmapG6Graph = this.graph;

        this.clearRelationEdges();
        this.drawRelationEdges();
        this.applyNodeStates();
        if (!this.showEdges) {
            this.hideAllEdges();
        } else {
            this.startEdgeFlows();
        }
        this.focusOnLowestMastery();
        this.graph.paint();
        this.setupFocusListener();
    }

    bindEdgeTooltip() {
        if (!this.graph || !this.showEdges) return;
        const tooltipEl = document.createElement('div');
        tooltipEl.style.position = 'fixed';
        tooltipEl.style.pointerEvents = 'none';
        tooltipEl.style.zIndex = '9999';
        tooltipEl.style.display = 'none';
        tooltipEl.style.background = 'rgba(15,23,42,0.95)';
        tooltipEl.style.color = '#e2e8f0';
        tooltipEl.style.padding = '8px 10px';
        tooltipEl.style.borderRadius = '8px';
        tooltipEl.style.boxShadow = '0 10px 30px rgba(0,0,0,0.18)';
        tooltipEl.style.fontSize = '12px';
        tooltipEl.style.lineHeight = '1.4';
        document.body.appendChild(tooltipEl);

        const show = (html, x, y) => {
            tooltipEl.innerHTML = html;
            tooltipEl.style.left = `${x + 12}px`;
            tooltipEl.style.top = `${y + 12}px`;
            tooltipEl.style.display = 'block';
        };
        const hide = () => {
            tooltipEl.style.display = 'none';
        };

        const buildHtml = (model) => {
            return `
                <div style="font-weight:700;font-size:13px;margin-bottom:4px;">${model.label || '关联'}</div>
                <div style="font-size:12px;">${model.source || ''} → ${model.target || ''}</div>
                ${model.comment ? `<div style="font-size:11px;color:#cbd5e1;margin-top:4px;white-space:pre-line;">${model.comment}</div>` : ''}
            `;
        };

        this.graph.on('edge:mouseenter', (evt) => {
            const model = evt?.item?.getModel?.() || {};
            const { clientX, clientY } = evt;
            show(buildHtml(model), clientX, clientY);
        });

        this.graph.on('edge:mouseleave', () => hide());
    }

    redrawRelationEdges() {
        if (!this.showRelationEdges || !this.showEdges) return;
        this.clearRelationEdges();
        this.drawRelationEdges();
    }

    forceCollapseNodes() {
        if (!this.graph) return;
    }

    handleNodeClick(evt) {
        if (this.clickTimer) {
            clearTimeout(this.clickTimer);
            this.clickTimer = null;
        }

        this.clickTimer = setTimeout(() => {
            const model = evt.item?.getModel();
            if (!model) return;
            // if (model.locked) return;

            this.graph.getNodes().forEach((node) => {
                this.graph.clearItemStates(node);
            });
            this.graph.setItemState(evt.item, 'selected', true);
            this.flashNode(evt.item);
            if (this.emitSelection) {
                this.notifySelection(model);
            }
            this.showTooltip(evt, model);
        }, this.clickDelay);
    }

    focusNodeById(nodeId) {
        if (!this.graph || !nodeId) return;
        // 展开到目标节点
        this.expandAncestors(nodeId);
        this.graph.layout?.();
        this.redrawRelationEdges();

        const target = this.graph.findById(nodeId);
        if (!target) return;

        this.graph.getNodes().forEach((node) => {
            this.graph.clearItemStates(node);
        });
        this.graph.setItemState(target, 'selected', true);
        this.flashNode(target);
        this.graph.focusItem(target, true, { duration: 400, easing: 'easeCubic' });
        this.graph.paint();
    }

    expandAncestors(nodeId) {
        let cur = nodeId;
        while (cur) {
            const parentId = this.parentMap[cur];
            if (parentId && this.graph.findById(parentId)) {
                this.graph.updateItem(this.graph.findById(parentId), { collapsed: false });
            }
            cur = parentId;
        }
    }

    setupFocusListener() {
        if (this.focusListener) {
            window.removeEventListener('mindmap-focus-node', this.focusListener);
        }
        this.focusListener = (evt) => {
            const targetId = evt.detail?.id || evt.detail || null;
            if (!targetId) return;
            this.focusNodeById(targetId);
        };
        window.addEventListener('mindmap-focus-node', this.focusListener);
    }

    forceCollapse() {
        if (!this.graph) {
            setTimeout(() => this.forceCollapse(), 200);
            return;
        }
        const nodes = this.graph.getNodes();
        let collapsedCount = 0;

        nodes.forEach(node => {
            const model = node.getModel();
            // 折叠除根节点外的所有有子节点的节点
            if (
                this.masteryData[model.id] ||
                (model.meta && this.masteryData[model.meta.code])
            ) {
                model.collapsed = false;
                return;
            }

            if (model.depth > 0 && model.children && model.children.length > 0) {
                try {
                    this.graph.collapseItem(node);
                    collapsedCount++;
                } catch (error) {
                    console.error('折叠节点失败:', model.id, error);
                }
            }
        });

        // 重新渲染和适配视图
        this.graph.refresh();
        this.graph.fitView(12);
    }

    resizeGraph() {
        if (!this.graph) return;
        const container = document.getElementById(this.containerId);
        if (!container) return;
        this.graph.changeSize(container.clientWidth, container.clientHeight);
        this.graph.fitView(12);
    }

    setMasteryData(payload = {}) {
        this.masteryData = this.normalizeMasteryPayload(payload);
        this.masteryCache = {};
        this.logMasteryCoverage();
    }

    normalizeMasteryPayload(payload = {}) {
        // 支持 {masteries: []} / {data: []} / {Target: {KP: {...}}} / 直接的键值对
        const map = {};
        const addEntry = (entry, fallbackKey = null) => {
            if (!entry || typeof entry !== 'object') return;
            const code =
                entry.kp_code ||
                entry.code ||
                entry.id ||
                fallbackKey;
            if (!code) return;
            const masteryLevel = this.normalizeMasteryLevel(
                entry.mastery_level
            );
            map[code] = {
                ...entry,
                kp_code: code,
                mastery_level: masteryLevel,
            };
        };

        const normalizeCandidate = (candidate) => {
            if (!candidate) return;
            if (Array.isArray(candidate)) {
                candidate.forEach((item) => addEntry(item));
                return;
            }
            if (typeof candidate === 'object') {
                Object.entries(candidate).forEach(([key, value]) =>
                    addEntry(value, key)
                );
            }
        };

        normalizeCandidate(payload.masteries);
        normalizeCandidate(payload.data);
        normalizeCandidate(payload.Target);
        // 允许直接传入键值对
        normalizeCandidate(payload);

        return map;
    }

    normalizeMasteryLevel(value) {
        const num = this.toNumber(value);
        if (!Number.isFinite(num)) return 0;
        return Math.max(0, Math.min(1, num));
    }

    toNumber(value) {
        const num = Number(value);
        return Number.isFinite(num) ? num : 0;
    }

    repaintNodes() {
        if (!this.graph || !this.treeData) return;
        const nodeMap = {};
        const walk = (node) => {
            if (!node) return;
            nodeMap[node.id] = node;
            (node.children || []).forEach((child) => walk(child));
        };
        walk(this.treeData);

        let updatedCount = 0;
        let firstUpdated = null;
        this.graph.getNodes().forEach((node) => {
            const id = node.getModel().id;
            const freshModel = nodeMap[id];
            if (!freshModel) return;
            // 确保 meta 中 has_mastery 等字段存在
            const masteryInfo =
                this.masteryData[id] ||
                this.masteryData[freshModel.meta?.code] ||
                null;
            freshModel.meta = {
                ...freshModel.meta,
                mastery_level: this.normalizeMasteryLevel(
                    masteryInfo?.mastery_level ?? freshModel.meta?.mastery_level
                ),
                has_mastery: Boolean(masteryInfo),
                mastery_info: masteryInfo,
                total_attempts: this.toNumber(
                    masteryInfo?.total_attempts ?? freshModel.meta?.total_attempts
                ),
            };
            this.graph.updateItem(node, freshModel);
            this.graph.refreshItem?.(node);
            this.applyDirectStyles(node, freshModel.meta.mastery_level, freshModel.meta.has_mastery);
            // 直接打上选中状态，避免样式被缓存
            if (freshModel.meta.has_mastery) {
                this.graph.setItemState(node, 'selected', true);
            } else {
                this.graph.clearItemStates(node, ['selected']);
            }
            updatedCount += 1;
            if (!firstUpdated) {
                firstUpdated = {
                    id,
                    mastery: freshModel.meta?.mastery_level,
                    has_mastery: freshModel.meta?.has_mastery,
                };
            }
        });

        // 强制重绘，避免样式缓存
        this.graph.paint();
    }

    applyDirectStyles(node, mastery, hasMastery) {
        // 直接对关键 shape 赋色，避免 G6 缓存导致颜色不变
        const keyShape = node.getKeyShape?.();
        const group = node.getContainer?.();
        if (!keyShape || !group) return;

        const palette = (() => {
            if (!hasMastery) {
                return { fill: '#f8fafc', stroke: '#cbd5e1', card: '#ffffff' };
            }
            if (mastery >= 0.8) return { fill: '#fffbeb', stroke: '#d3b55f', card: '#ffffff' };
            if (mastery >= 0.6) return { fill: '#ecfdf3', stroke: '#34d399', card: '#ffffff' };
            if (mastery >= 0.4) return { fill: '#fffbeb', stroke: '#f59e0b', card: '#fff7ed' };
            return { fill: '#fef2f2', stroke: '#f87171', card: '#fff1f2' };
        })();

        keyShape.attr({
            fill: palette.fill,
            stroke: palette.stroke,
        });

        const card = group.find((e) => e.get?.('name') === 'card-shape');
        if (card) {
            card.attr({ fill: palette.card });
        }
        // 额外高亮轮廓，确保视觉可见
        this.graph.setItemState(node, 'selected', hasMastery);
        this.graph.refreshItem?.(node);
    }

    logMasteryCoverage() {
        if (!this.nodeIdSet || !this.nodeIdSet.size) return;
        const masteries = Object.keys(this.masteryData || {});
        if (!masteries.length) return;
        const missing = masteries.filter((id) => !this.nodeIdSet.has(id));
        if (missing.length) {
            console.warn(
                '掌握度返回的知识点未在图谱中找到:',
                missing.slice(0, 20),
                missing.length > 20 ? `...共${missing.length}条` : ''
            );
        }
    }

    buildParentMap(node, parentId = null) {
        if (!node) return;
        this.parentMap[node.id] = parentId;
        (node.children || []).forEach((child) =>
            this.buildParentMap(child, node.id)
        );
    }

    hideAllEdges() {
        if (!this.graph) return;
        this.graph.getEdges().forEach((edge) => {
            this.graph.hideItem(edge);
        });
    }

}

// 定义KnowledgeMindmapGraph类，确保G6已加载
function defineGraphClass() {
    if (typeof window.G6 === 'undefined') {
        setTimeout(defineGraphClass, 100);
        return;
    }
    window.KnowledgeMindmapGraph = KnowledgeMindmapGraph;
}

// 启动定义流程
defineGraphClass();
