// ==========================================
// NotesPane Kit — NoteGraph (Canvas)
// ==========================================
// Force-directed graph visualization of note connections.
// Self-contained Preact component with physics sim,
// pan/zoom/drag, hover highlighting, folder colors.
// Lazy-loadable via dynamic import().
// Independently importable.

const { useRef, useEffect, useState, useCallback } = window.hooks;
const html = window.html;

// ── Simulation Parameters ───────────────────
const SIM = {
    repulsion:     800,
    attraction:    0.006,
    edgeLength:    130,
    damping:       0.88,
    centerGravity: 0.012,
    maxVelocity:   8,
    ticksPerFrame: 3,
    coolThreshold: 0.5,
};

const BASE_RADIUS = 6;

const FOLDER_COLORS = [
    '#6366f1', '#ec4899', '#f59e0b', '#10b981', '#3b82f6',
    '#8b5cf6', '#ef4444', '#06b6d4', '#84cc16', '#f97316',
];

function _clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

function _withAlpha(hex, alpha) {
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    return `rgba(${r},${g},${b},${alpha})`;
}

/**
 * @param {{
 *   data: { nodes: Array, edges: Array, unresolved?: Array } | null,
 *   onNodeClick: (id: string) => void,
 *   onGhostClick: (title: string) => void,
 *   onBack: () => void,
 *   handleRef?: { current: any },
 * }} props
 */
export function NoteGraph(props) {
    const { data, onNodeClick, onGhostClick, onBack, handleRef } = props;

    const canvasRef = useRef(null);
    const wrapRef = useRef(null);
    const stateRef = useRef(null);
    const [showOrphans, setShowOrphans] = useState(true);
    const [focusNodeId, setFocusNodeId] = useState(null);
    const [stats, setStats] = useState({ nodes: 0, edges: 0 });

    // ── Prepare graph data with physics state ──
    useEffect(() => {
        if (!data || !canvasRef.current) return;

        const canvas = canvasRef.current;
        const rect = wrapRef.current.getBoundingClientRect();
        canvas.width = Math.round(rect.width);
        canvas.height = Math.round(rect.height);

        const cx = canvas.width / 2;
        const cy = canvas.height / 2;

        // Add physics state to nodes
        const nodeMap = {};
        const folderColorMap = {};

        function folderColor(path) {
            if (!path || path === '/') return '#6b7280';
            if (!folderColorMap[path]) {
                const idx = Object.keys(folderColorMap).length % FOLDER_COLORS.length;
                folderColorMap[path] = FOLDER_COLORS[idx];
            }
            return folderColorMap[path];
        }

        const nodes = data.nodes.map(n => {
            const node = {
                ...n,
                x: cx + (Math.random() - 0.5) * canvas.width * 0.5,
                y: cy + (Math.random() - 0.5) * canvas.height * 0.5,
                vx: 0, vy: 0, ax: 0, ay: 0,
                pinned: false, isGhost: false,
            };
            nodeMap[n.id] = node;
            return node;
        });

        // Resolve edge references
        const edges = data.edges.map(e => ({
            ...e,
            sourceNode: nodeMap[e.source],
            targetNode: nodeMap[e.target],
        }));

        // Ghost nodes for unresolved links
        const ghostNodes = [];
        if (data.unresolved) {
            const ghostMap = {};
            for (const d of data.unresolved) {
                const key = d.title.toLowerCase();
                if (!ghostMap[key]) {
                    const ghost = {
                        id: '__ghost_' + key, title: d.title,
                        folder_path: '', tags: [], link_count: 0,
                        x: cx + (Math.random() - 0.5) * canvas.width * 0.4,
                        y: cy + (Math.random() - 0.5) * canvas.height * 0.4,
                        vx: 0, vy: 0, ax: 0, ay: 0,
                        pinned: false, isGhost: true,
                    };
                    ghostMap[key] = ghost;
                    ghostNodes.push(ghost);
                }
            }
        }

        setStats({ nodes: nodes.length, edges: edges.length });

        const state = {
            nodes, edges, ghostNodes, nodeMap, folderColorMap, folderColor,
            panX: 0, panY: 0, zoom: 1,
            animId: null, canvas, ctx: canvas.getContext('2d'),
            hoveredNode: null, dragNode: null,
            isDragging: false, lastMouse: null,
            showOrphans: true,
            focusNodeId: null,
            onNodeClick, onGhostClick,
            onFocusNode: (id) => setFocusNodeId(id),
        };

        stateRef.current = state;

        // Start simulation
        _startSim(state, onNodeClick);

        // Attach listeners
        const cleanup = _attachListeners(state, onNodeClick);

        // Resize observer
        let roFrame = null;
        const ro = new ResizeObserver(() => {
            if (roFrame) return;
            roFrame = requestAnimationFrame(() => {
                roFrame = null;
                const r = wrapRef.current.getBoundingClientRect();
                const w = Math.round(r.width);
                const h = Math.round(r.height);
                if (canvas.width !== w || canvas.height !== h) {
                    canvas.width = w;
                    canvas.height = h;
                    _render(state);
                }
            });
        });
        ro.observe(wrapRef.current);

        return () => {
            if (state.animId) cancelAnimationFrame(state.animId);
            state.animId = null;
            cleanup();
            ro.disconnect();
        };
    }, [data, onNodeClick]);

    // ── Sync showOrphans + focusNodeId into state ──
    useEffect(() => {
        if (stateRef.current) {
            stateRef.current.showOrphans = showOrphans;
            stateRef.current.focusNodeId = focusNodeId;
            _render(stateRef.current);
        }
    }, [showOrphans, focusNodeId]);

    // ── Expose imperative handle ────────────
    useEffect(() => {
        if (handleRef) {
            handleRef.current = {
                resetZoom() {
                    const s = stateRef.current;
                    if (!s) return;
                    s.panX = 0; s.panY = 0; s.zoom = 1;
                    _render(s);
                },
                toggleOrphans() {
                    setShowOrphans(p => !p);
                },
                focusNode(id) {
                    setFocusNodeId(id);
                },
                clearFocus() {
                    setFocusNodeId(null);
                },
            };
        }
    });

    // ── Focus mode label ───────────────────
    const focusLabel = focusNodeId && stateRef.current
        ? (stateRef.current.nodes.find(n => n.id === focusNodeId)?.title || 'Unknown')
        : null;

    return html`
        <div class="sw-notes-pane__graph">
            <div class="sw-notes-pane__graph-toolbar">
                <button class="sw-notes-pane__toolbar-btn" onClick=${onBack}>← Back</button>
                <span class="sw-notes-pane__graph-stats">
                    ${stats.nodes} notes · ${stats.edges} links
                </span>
                <span class="sw-notes-pane__toolbar-spacer" />
                ${focusNodeId && html`
                    <span style="font-size:11px;color:var(--accent)">Focus: ${focusLabel}</span>
                    <button class="sw-notes-pane__toolbar-btn" onClick=${() => setFocusNodeId(null)}>
                        Show All
                    </button>`}
                <button class="sw-notes-pane__toolbar-btn"
                        onClick=${() => setShowOrphans(p => !p)}>
                    Orphans ${showOrphans ? 'On' : 'Off'}
                </button>
                <button class="sw-notes-pane__toolbar-btn"
                        onClick=${() => handleRef?.current?.resetZoom()}>
                    Reset
                </button>
            </div>
            <div class="sw-notes-pane__graph-canvas-wrap" ref=${wrapRef}>
                <canvas ref=${canvasRef} />
            </div>
        </div>
    `;
}

// ── Simulation ──────────────────────────────

function _allNodes(state) {
    const nodes = [...state.nodes];
    if (state.showOrphans !== false && state.ghostNodes) {
        nodes.push(...state.ghostNodes);
    }
    return nodes;
}

function _startSim(state, onNodeClick) {
    function frame() {
        for (let t = 0; t < SIM.ticksPerFrame; t++) _tick(state);
        _render(state);

        let totalKE = 0;
        for (const n of _allNodes(state)) {
            totalKE += n.vx * n.vx + n.vy * n.vy;
        }
        if (totalKE > SIM.coolThreshold || state.dragNode) {
            state.animId = requestAnimationFrame(frame);
        } else {
            state.animId = null;
            _render(state);
        }
    }

    if (state.animId) cancelAnimationFrame(state.animId);
    state.animId = requestAnimationFrame(frame);
}

function _tick(state) {
    const nodes = _allNodes(state);
    const edges = state.edges;
    if (nodes.length === 0) return;

    for (const n of nodes) { n.ax = 0; n.ay = 0; }

    // Repulsion
    for (let i = 0; i < nodes.length; i++) {
        for (let j = i + 1; j < nodes.length; j++) {
            const dx = nodes[j].x - nodes[i].x;
            const dy = nodes[j].y - nodes[i].y;
            const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
            const f = SIM.repulsion / (dist * dist);
            const fx = (dx / dist) * f;
            const fy = (dy / dist) * f;
            nodes[i].ax -= fx; nodes[i].ay -= fy;
            nodes[j].ax += fx; nodes[j].ay += fy;
        }
    }

    // Attraction (edges)
    for (const e of edges) {
        const s = e.sourceNode, t = e.targetNode;
        if (!s || !t) continue;
        const dx = t.x - s.x, dy = t.y - s.y;
        const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
        const f = SIM.attraction * (dist - SIM.edgeLength);
        const fx = (dx / dist) * f;
        const fy = (dy / dist) * f;
        s.ax += fx; s.ay += fy;
        t.ax -= fx; t.ay -= fy;
    }

    // Center gravity + integration
    const cx = state.canvas.width / 2, cy = state.canvas.height / 2;
    for (const n of nodes) {
        if (n.pinned) continue;
        n.ax += (cx - n.x) * SIM.centerGravity;
        n.ay += (cy - n.y) * SIM.centerGravity;
        n.vx = _clamp((n.vx + n.ax) * SIM.damping, -SIM.maxVelocity, SIM.maxVelocity);
        n.vy = _clamp((n.vy + n.ay) * SIM.damping, -SIM.maxVelocity, SIM.maxVelocity);
        n.x += n.vx;
        n.y += n.vy;
    }
}

// ── Rendering ───────────────────────────────

function _render(state) {
    const { ctx, canvas, panX, panY, zoom, hoveredNode, focusNodeId } = state;
    if (!ctx) return;

    const nodes = _allNodes(state);
    const edges = state.edges;

    // Build focus neighbor set for focus mode
    const focusNeighborIds = new Set();
    if (focusNodeId) {
        focusNeighborIds.add(focusNodeId);
        for (const e of edges) {
            if (e.source === focusNodeId) focusNeighborIds.add(e.target);
            if (e.target === focusNodeId) focusNeighborIds.add(e.source);
        }
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save();
    ctx.translate(panX, panY);
    ctx.scale(zoom, zoom);

    // Edges
    for (const e of edges) {
        const s = e.sourceNode, t = e.targetNode;
        if (!s || !t) continue;

        // Focus mode: hide edges not touching the focus neighborhood
        if (focusNodeId && !focusNeighborIds.has(e.source) && !focusNeighborIds.has(e.target)) continue;
        const isFocusDimmed = focusNodeId && !(focusNeighborIds.has(e.source) && focusNeighborIds.has(e.target));

        const isHighlighted = hoveredNode && (s.id === hoveredNode.id || t.id === hoveredNode.id);

        ctx.beginPath();
        ctx.moveTo(s.x, s.y);
        ctx.lineTo(t.x, t.y);

        if (e.is_transclusion) {
            ctx.setLineDash([4, 4]);
            ctx.strokeStyle = isHighlighted ? 'rgba(99,102,241,0.8)'
                : isFocusDimmed ? 'rgba(99,102,241,0.08)' : 'rgba(99,102,241,0.3)';
        } else {
            ctx.setLineDash([]);
            ctx.strokeStyle = isHighlighted ? 'rgba(160,160,160,0.8)'
                : isFocusDimmed ? 'rgba(120,120,120,0.06)' : 'rgba(120,120,120,0.25)';
        }
        ctx.lineWidth = isHighlighted ? 2 : 1;
        ctx.stroke();
    }
    ctx.setLineDash([]);

    // Nodes
    for (const n of nodes) {
        if (n.isGhost && !state.showOrphans) continue;

        const r = n.isGhost ? BASE_RADIUS * 0.6 : BASE_RADIUS + Math.sqrt(n.link_count || 0) * 2;
        const isHovered = hoveredNode && n.id === hoveredNode.id;
        const isNeighbor = hoveredNode && _isNeighbor(state, hoveredNode, n);
        const isFocusVisible = !focusNodeId || focusNeighborIds.has(n.id);
        const isDimmed = (hoveredNode && !isHovered && !isNeighbor) || !isFocusVisible;

        ctx.beginPath();
        ctx.arc(n.x, n.y, r, 0, Math.PI * 2);

        if (n.isGhost) {
            ctx.fillStyle = isDimmed ? 'rgba(200,50,50,0.1)' : 'rgba(200,50,50,0.4)';
            ctx.setLineDash([2, 2]);
            ctx.strokeStyle = 'rgba(200,50,50,0.5)';
        } else if (isHovered) {
            ctx.fillStyle = '#6366f1';
        } else {
            const color = state.folderColor(n.folder_path);
            ctx.fillStyle = isDimmed ? _withAlpha(color, 0.15) : _withAlpha(color, 0.7);
            ctx.strokeStyle = isDimmed ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.15)';
        }

        ctx.fill();
        if (!n.isGhost) {
            ctx.setLineDash([]);
            ctx.lineWidth = isHovered ? 2 : 1;
            ctx.stroke();
        } else {
            ctx.stroke();
            ctx.setLineDash([]);
        }

        // Labels
        if (zoom > 0.5 && (isHovered || isNeighbor || !hoveredNode)) {
            const fontSize = Math.max(10, 11 / zoom);
            ctx.font = `${n.isGhost ? 'italic ' : ''}${fontSize}px var(--font, system-ui, sans-serif)`;
            ctx.fillStyle = isDimmed ? 'rgba(200,200,200,0.2)' : 'rgba(220,220,220,0.9)';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'top';
            const label = n.title.length > 30 ? n.title.slice(0, 28) + '\u2026' : n.title;
            ctx.fillText(label, n.x, n.y + r + 4);
        }
    }

    ctx.restore();
}

function _isNeighbor(state, hovered, candidate) {
    for (const e of state.edges) {
        if ((e.source === hovered.id && e.target === candidate.id) ||
            (e.target === hovered.id && e.source === candidate.id)) {
            return true;
        }
    }
    return false;
}

// ── Interaction ─────────────────────────────

function _attachListeners(state, onNodeClick) {
    const canvas = state.canvas;

    function toGraph(e) {
        const rect = canvas.getBoundingClientRect();
        return {
            x: (e.clientX - rect.left - state.panX) / state.zoom,
            y: (e.clientY - rect.top - state.panY) / state.zoom,
        };
    }

    function hitTest(gx, gy) {
        const nodes = _allNodes(state);
        for (let i = nodes.length - 1; i >= 0; i--) {
            const n = nodes[i];
            const r = n.isGhost ? BASE_RADIUS * 0.6 : BASE_RADIUS + Math.sqrt(n.link_count || 0) * 2;
            const dx = n.x - gx, dy = n.y - gy;
            if (dx * dx + dy * dy <= (r + 4) * (r + 4)) return n;
        }
        return null;
    }

    const DRAG_THRESHOLD = 5; // px — must move at least this far to count as drag

    const onMouseDown = (e) => {
        const g = toGraph(e);
        const node = hitTest(g.x, g.y);
        if (node) {
            state.dragNode = node;
            if (!node.isGhost) node.pinned = true;
            state.isDragging = false;
        }
        state.lastMouse = { x: e.clientX, y: e.clientY };
        state.mouseDownPos = { x: e.clientX, y: e.clientY };
    };

    const onMouseMove = (e) => {
        const g = toGraph(e);
        if (state.dragNode) {
            // Only count as drag if moved past threshold
            if (!state.isDragging && state.mouseDownPos) {
                const dx = e.clientX - state.mouseDownPos.x;
                const dy = e.clientY - state.mouseDownPos.y;
                if (dx * dx + dy * dy < DRAG_THRESHOLD * DRAG_THRESHOLD) return;
            }
            state.isDragging = true;
            state.dragNode.x = g.x;
            state.dragNode.y = g.y;
            state.dragNode.vx = 0;
            state.dragNode.vy = 0;
            if (!state.animId) _startSim(state, onNodeClick);
        } else if (state.lastMouse && e.buttons === 1) {
            state.panX += e.clientX - state.lastMouse.x;
            state.panY += e.clientY - state.lastMouse.y;
            state.lastMouse = { x: e.clientX, y: e.clientY };
            _render(state);
        } else {
            const node = hitTest(g.x, g.y);
            if (node !== state.hoveredNode) {
                state.hoveredNode = node;
                canvas.style.cursor = node ? 'pointer' : 'default';
                _render(state);
            }
        }
    };

    const onMouseUp = (e) => {
        if (state.dragNode) {
            const clicked = state.dragNode;
            clicked.pinned = false;
            if (!state.isDragging) {
                if (clicked.isGhost && state.onGhostClick) {
                    // Ghost node → create note with that title
                    state.onGhostClick(clicked.title);
                } else if (!clicked.isGhost) {
                    // Single click → focus on this node (show neighbors)
                    if (state.onFocusNode) {
                        state.onFocusNode(clicked.id === state.focusNodeId ? null : clicked.id);
                    }
                }
            }
            state.dragNode = null;
        } else if (!state.isDragging) {
            // Clicked empty space → clear focus
            if (state.onFocusNode && state.focusNodeId) {
                state.onFocusNode(null);
            }
        }
        state.lastMouse = null;
        state.isDragging = false;
    };

    // Double-click → open note
    const onDblClick = (e) => {
        const g = toGraph(e);
        const node = hitTest(g.x, g.y);
        if (node && !node.isGhost && onNodeClick) {
            onNodeClick(node.id);
        } else if (node && node.isGhost && state.onGhostClick) {
            state.onGhostClick(node.title);
        }
    };

    const onWheel = (e) => {
        e.preventDefault();
        const factor = e.deltaY > 0 ? 0.92 : 1.08;
        const newZoom = _clamp(state.zoom * factor, 0.15, 4.0);
        const rect = canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left;
        const my = e.clientY - rect.top;
        state.panX = mx - (mx - state.panX) * (newZoom / state.zoom);
        state.panY = my - (my - state.panY) * (newZoom / state.zoom);
        state.zoom = newZoom;
        _render(state);
    };

    canvas.addEventListener('mousedown', onMouseDown);
    canvas.addEventListener('mousemove', onMouseMove);
    canvas.addEventListener('mouseup', onMouseUp);
    canvas.addEventListener('dblclick', onDblClick);
    canvas.addEventListener('wheel', onWheel, { passive: false });

    return () => {
        canvas.removeEventListener('mousedown', onMouseDown);
        canvas.removeEventListener('mousemove', onMouseMove);
        canvas.removeEventListener('mouseup', onMouseUp);
        canvas.removeEventListener('dblclick', onDblClick);
        canvas.removeEventListener('wheel', onWheel);
    };
}
