/**
 * SVG node rendering for the graph editor.
 */

import { ENTITY_COLORS, NODE_WIDTH, NODE_HEIGHT, computePorts } from './graph-layout.ts';
import { EDGE_COLORS } from './graph-edges.ts';
import { createColorPicker, registerColorPicker, closeAllColorPickers } from './color-picker.ts';
import { getCardColor, setCardColor } from './card-colors.ts';
import * as P from './icon-paths.ts';

const SVG_NS = 'http://www.w3.org/2000/svg';

/* ── Types ────────────────────────────────────────────────────── */

interface PortInfo {
    types: string[];
    ports: Record<string, number>;
}

interface GraphNode {
    id: string;
    kind: string;
    name: string;
    subtype?: string;
    x: number;
    y: number;
    width: number;
    height: number;
    running?: boolean;
    inputPorts?: PortInfo;
    outputPorts?: PortInfo;
}

interface GraphEdge {
    from: string;
    to: string;
    type: string;
    field?: string;
}

interface NodeCallbacks {
    onNodeClick?: (node: GraphNode, e: MouseEvent) => void;
    onNodeDblClick?: (node: GraphNode, e: MouseEvent) => void;
    onDeleteNode?: (node: GraphNode) => void;
    onEditNode?: (node: GraphNode) => void;
    onTestNode?: (node: GraphNode) => void;
    onStartStopNode?: (node: GraphNode) => void;
    onNotificationTest?: (node: GraphNode) => void;
    onCloneNode?: (node: GraphNode) => void;
    onActivatePreset?: (node: GraphNode) => void;
}

interface OverlayButton {
    icon?: string;
    svgPath?: string;
    action: string;
    cls: string;
    scale?: number;
}

// ── Port type → human-readable label ──
const PORT_LABELS = {
    template:   'Template',
    picture:    'Picture',
    colorstrip: 'Strip',
    value:      'Value',
    audio:      'Audio',
    clock:      'Clock',
    scene:      'Scene',
    device:     'Device',
    default:    'Ref',
};

// ── Entity kind → default icon path data ──
const KIND_ICONS = {
    device:              P.monitor,
    capture_template:    P.camera,
    pp_template:         P.wrench,
    audio_template:      P.music,
    pattern_template:    P.fileText,
    picture_source:      P.tv,
    audio_source:        P.music,
    value_source:        P.hash,
    color_strip_source:  P.film,
    sync_clock:          P.clock,
    output_target:       P.zap,
    scene_preset:        P.sparkles,
    automation:          P.clipboardList,
    cspt:                P.wrench,
};

// ── Subtype-specific icon overrides ──
const SUBTYPE_ICONS = {
    color_strip_source: {
        picture_advanced: P.monitor, static: P.palette, color_cycle: P.refreshCw,
        gradient: P.rainbow, effect: P.zap, composite: P.link,
        mapped: P.mapPin, mapped_zones: P.mapPin,
        audio: P.music, audio_visualization: P.music,
        api_input: P.send, notification: P.bellRing, daylight: P.sun, candlelight: P.flame,
        processed: P.sparkles,
    },
    picture_source: { raw: P.monitor, processed: P.palette, static_image: P.image, video: P.film },
    value_source: {
        static: P.layoutDashboard, animated: P.refreshCw, audio: P.music,
        adaptive_time: P.clock, adaptive_scene: P.cloudSun, daylight: P.sun,
    },
    audio_source: { capture: P.volume2, processed: P.slidersHorizontal },
    output_target: { led: P.lightbulb, wled: P.lightbulb, ha_light: P.lightbulb },
};

function svgEl(tag: string, attrs: Record<string, string | number> = {}): SVGElement {
    const el = document.createElementNS(SVG_NS, tag);
    for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
    return el;
}

/** Truncate text to fit within maxWidth (approximate). */
function truncate(text: string, maxChars: number = 22): string {
    if (!text) return '';
    return text.length > maxChars ? text.slice(0, maxChars - 1) + '\u2026' : text;
}

/**
 * Render all nodes into the given SVG group.
 * @param {SVGGElement} group
 * @param {Map} nodeMap - id → {id, kind, name, subtype, x, y, width, height, running}
 * @param {Object} callbacks - { onNodeClick, onNodeDblClick, onDeleteNode, onEditNode, onTestNode }
 */
export function renderNodes(group: SVGGElement, nodeMap: Map<string, GraphNode>, callbacks: NodeCallbacks = {}): void {
    // Clear existing
    while (group.firstChild) group.firstChild.remove();

    for (const node of nodeMap.values()) {
        const g = renderNode(node, callbacks);
        group.appendChild(g);
    }
}

/**
 * Render a single node.
 */

/** Return custom color for a node, or null if none set. */
export function getNodeColor(nodeId: string): string | null {
    return getCardColor(nodeId) || null;
}

/** Return color for a node: custom if set, else entity-type default. Used by minimap/search. */
export function getNodeDisplayColor(nodeId: string, kind: string): string {
    return getNodeColor(nodeId) || ENTITY_COLORS[kind] || '#666';
}

/** Open a color picker for a graph node, positioned near the click point. */
function _openNodeColorPicker(node: GraphNode, e: MouseEvent): void {
    const nodeEl = (e.target as SVGElement).closest('.graph-node') as SVGElement | null || document.querySelector(`.graph-node[data-id="${node.id}"]`) as SVGElement | null;
    if (!nodeEl) return;
    const svg = (nodeEl as SVGGraphicsElement).ownerSVGElement;
    const container = svg?.closest('.graph-container');
    if (!svg || !container) return;

    // Remove any previous graph color picker overlay
    container.querySelector('.graph-cp-overlay')?.remove();
    closeAllColorPickers();

    // Compute position relative to container
    const cr = container.getBoundingClientRect();
    const px = e.clientX - cr.left;
    const py = e.clientY - cr.top;

    // Create an HTML overlay with the custom color picker
    const pickerId = `graph-node-${node.id}`;
    const cpOverlay = document.createElement('div');
    cpOverlay.className = 'graph-cp-overlay';
    cpOverlay.style.cssText = `position:absolute; left:${px}px; top:${py}px; z-index:100;`;
    const curColor = getNodeColor(node.id);
    cpOverlay.innerHTML = createColorPicker({
        id: pickerId,
        currentColor: curColor || ENTITY_COLORS[node.kind] || '#666',
        onPick: undefined,
        anchor: 'left',
        showReset: true,
        resetColor: '#808080',
    });
    container.appendChild(cpOverlay);

    // Register callback to update the bar color
    registerColorPicker(pickerId, (hex) => {
        const bar = nodeEl.querySelector('.graph-node-color-bar') as SVGElement | null;
        const barCover = bar?.nextElementSibling as SVGElement | null;
        if (bar) {
            if (hex) {
                bar.setAttribute('fill', hex);
                if (barCover) barCover.setAttribute('fill', hex);
                bar.style.display = '';
                if (barCover) barCover.style.display = '';
            } else {
                bar.style.display = 'none';
                if (barCover) barCover.style.display = 'none';
            }
        }
        setCardColor(node.id, hex);
        cpOverlay.remove();
    });

    // Open the popover immediately
    window._cpToggle(pickerId);
}

function renderNode(node: GraphNode, callbacks: NodeCallbacks): SVGElement {
    const { id, kind, name, subtype, x, y, width, height, running } = node;
    let color = getNodeColor(id);

    const g = svgEl('g', {
        class: `graph-node${running ? ' running' : ''}`,
        'data-id': id,
        'data-kind': kind,
        transform: `translate(${x}, ${y})`,
    });

    // Body rect
    const body = svgEl('rect', {
        class: 'graph-node-body',
        x: 0, y: 0,
        width, height,
        rx: 8, ry: 8,
    });
    g.appendChild(body);

    // Color bar (left strip) — only visible when user has set a custom color
    const bar = svgEl('rect', {
        class: 'graph-node-color-bar',
        x: 0, y: 0,
        width: 6, height,
        rx: 8, ry: 8,
        fill: color || 'transparent',
    });
    if (!color) bar.style.display = 'none';
    g.appendChild(bar);
    // Cover the right side rounded corners of the bar
    const barCover = svgEl('rect', {
        x: 3, y: 0,
        width: 4, height,
        fill: color || 'transparent',
    });
    if (!color) barCover.style.display = 'none';
    g.appendChild(barCover);

    // Clickable color bar overlay (wider hit area)
    const barHit = svgEl('rect', {
        class: 'graph-node-color-bar-hit',
        x: 0, y: 0,
        width: 12, height,
        fill: 'transparent',
        cursor: 'pointer',
    });
    barHit.style.cursor = 'pointer';
    barHit.addEventListener('click', (e) => {
        e.stopPropagation();
        _openNodeColorPicker(node, e);
    });
    g.appendChild(barHit);

    // Input ports (left side)
    if (node.inputPorts?.types) {
        for (const t of node.inputPorts.types) {
            const py = node.inputPorts.ports[t];
            const dot = svgEl('circle', {
                class: `graph-port graph-port-in graph-port-${t}`,
                cx: 0, cy: py, r: 4,
                fill: EDGE_COLORS[t] || EDGE_COLORS.default,
                'data-node-id': id,
                'data-node-kind': kind,
                'data-port-type': t,
                'data-port-dir': 'in',
            });
            const tip = svgEl('title');
            tip.textContent = PORT_LABELS[t] || t;
            dot.appendChild(tip);
            g.appendChild(dot);
            // Port label (shown on hover) — outside node, left of port
            const lbl = svgEl('text', {
                class: 'graph-port-label graph-port-label-in',
                x: -8, y: py + 3,
                'text-anchor': 'end',
            });
            lbl.textContent = PORT_LABELS[t] || t;
            g.appendChild(lbl);
        }
    }

    // Output ports (right side)
    if (node.outputPorts?.types) {
        for (const t of node.outputPorts.types) {
            const py = node.outputPorts.ports[t];
            const dot = svgEl('circle', {
                class: `graph-port graph-port-out graph-port-${t}`,
                cx: width, cy: py, r: 4,
                fill: EDGE_COLORS[t] || EDGE_COLORS.default,
                'data-node-id': id,
                'data-node-kind': kind,
                'data-port-type': t,
                'data-port-dir': 'out',
            });
            const tip = svgEl('title');
            tip.textContent = PORT_LABELS[t] || t;
            dot.appendChild(tip);
            g.appendChild(dot);
            // Port label (shown on hover) — outside node, right of port
            const lbl = svgEl('text', {
                class: 'graph-port-label graph-port-label-out',
                x: width + 8, y: py + 3,
            });
            lbl.textContent = PORT_LABELS[t] || t;
            g.appendChild(lbl);
        }
    }

    // Entity icon (right side)
    const iconPaths = (SUBTYPE_ICONS[kind]?.[subtype]) || KIND_ICONS[kind];
    if (iconPaths) {
        const iconG = svgEl('g', {
            class: 'graph-node-icon',
            transform: `translate(${width - 28}, ${height / 2 - 8}) scale(0.667)`,
        });
        iconG.innerHTML = iconPaths;
        g.appendChild(iconG);
    }

    // Running dot
    if (running) {
        const dot = svgEl('circle', {
            class: 'graph-node-running-dot',
            cx: width - 14, cy: 14,
            r: 4,
        });
        g.appendChild(dot);
    }

    // Clip path for title/subtitle text (prevent overflow past icon area)
    const clipId = `clip-text-${id.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
    const clipPath = svgEl('clipPath', { id: clipId });
    clipPath.appendChild(svgEl('rect', { x: 14, y: 0, width: width - 48, height }));
    g.appendChild(clipPath);

    // Title (shift left edge for icon to have room)
    const title = svgEl('text', {
        class: 'graph-node-title',
        x: 16, y: 24,
        'clip-path': `url(#${clipId})`,
    });
    title.textContent = name;
    g.appendChild(title);

    // Subtitle (type)
    if (subtype) {
        const sub = svgEl('text', {
            class: 'graph-node-subtitle',
            x: 16, y: 42,
            'clip-path': `url(#${clipId})`,
        });
        sub.textContent = subtype.replace(/_/g, ' ');
        g.appendChild(sub);
    }

    // Hover overlay (action buttons)
    const overlay = _createOverlay(node, width, callbacks);
    g.appendChild(overlay);

    // Click handler
    g.addEventListener('click', (e) => {
        e.stopPropagation();
        if (callbacks.onNodeClick) callbacks.onNodeClick(node, e);
    });

    g.addEventListener('dblclick', (e) => {
        e.stopPropagation();
        if (callbacks.onNodeDblClick) callbacks.onNodeDblClick(node, e);
    });

    return g;
}

// Entity kinds that support start/stop (including automation enable/disable)
const START_STOP_KINDS = new Set(['output_target', 'sync_clock', 'automation']);

// Entity kinds that support test/preview
const TEST_KINDS = new Set([
    'capture_template', 'pp_template', 'audio_template',
    'picture_source', 'audio_source', 'value_source',
    'color_strip_source', 'cspt',
]);

function _createOverlay(node: GraphNode, nodeWidth: number, callbacks: NodeCallbacks): SVGElement {
    const overlay = svgEl('g', { class: 'graph-node-overlay' });
    const btnSize = 24;
    const btnGap = 2;

    // Build button list dynamically based on node kind/subtype
    const btns: OverlayButton[] = [];

    // Start/stop button for applicable kinds
    if (START_STOP_KINDS.has(node.kind)) {
        btns.push({
            icon: node.running ? '\u25A0' : '\u25B6', // ■ stop / ▶ play
            action: 'startstop',
            cls: node.running ? 'danger' : 'success',
        });
    }

    // Scene preset activation
    if (node.kind === 'scene_preset') {
        btns.push({ svgPath: P.play, action: 'activate', cls: 'success' });
    }

    // Test button for applicable kinds
    if (TEST_KINDS.has(node.kind)) {
        btns.push({ svgPath: P.flaskConical, action: 'test', cls: '' });
    }

    // Notification test for notification color strip sources
    if (node.kind === 'color_strip_source' && node.subtype === 'notification') {
        btns.push({ svgPath: P.bellRing, action: 'notify', cls: '' });
    }

    // Clone (smaller scale to fit the compact button)
    btns.push({ svgPath: P.copy, action: 'clone', cls: '', scale: 0.6 });

    // Color picker
    btns.push({ svgPath: P.palette, action: 'color', cls: '', scale: 0.6 });

    // Always: edit and delete
    btns.push({ icon: '\u270E', action: 'edit', cls: '' });   // ✎
    btns.push({ icon: '\u2716', action: 'delete', cls: 'danger' });  // ✖

    const totalW = btns.length * (btnSize + btnGap) - btnGap + 8;
    const ox = nodeWidth - totalW - 4;
    const oy = -btnSize - 6;

    // Invisible bridge rect so hover doesn't drop when moving to overlay
    overlay.appendChild(svgEl('rect', {
        x: ox, y: oy,
        width: totalW, height: btnSize + 10,
        fill: 'transparent',
        'pointer-events': 'all',
    }));

    // Background
    overlay.appendChild(svgEl('rect', {
        class: 'graph-node-overlay-bg',
        x: ox, y: oy,
        width: totalW, height: btnSize + 4,
    }));

    const ACTION_LABELS = {
        startstop: node.running ? 'Stop' : 'Start',
        activate: 'Activate preset',
        test: 'Test / Preview',
        notify: 'Test notification',
        clone: 'Clone',
        color: 'Color',
        edit: 'Edit',
        delete: 'Delete',
    };

    btns.forEach((btn, i) => {
        const bx = ox + 4 + i * (btnSize + btnGap);
        const by = oy + 2;
        const bg = svgEl('g', { class: `graph-node-overlay-btn ${btn.cls}` });
        bg.appendChild(svgEl('rect', { x: bx, y: by, width: btnSize, height: btnSize }));
        if (btn.svgPath) {
            const s = btn.scale || ((btnSize - 4) / 24);
            const iconSize = 24 * s;
            const pad = (btnSize - iconSize) / 2;
            const iconG = svgEl('g', {
                transform: `translate(${bx + pad}, ${by + pad}) scale(${s})`,
            });
            iconG.innerHTML = btn.svgPath;
            iconG.setAttribute('fill', 'none');
            iconG.setAttribute('stroke', 'currentColor');
            iconG.setAttribute('stroke-width', '2');
            iconG.setAttribute('stroke-linecap', 'round');
            iconG.setAttribute('stroke-linejoin', 'round');
            bg.appendChild(iconG);
        } else {
            const txt = svgEl('text', { x: bx + btnSize / 2, y: by + btnSize / 2 });
            txt.textContent = btn.icon ?? '';
            bg.appendChild(txt);
        }
        const btnTip = svgEl('title');
        btnTip.textContent = ACTION_LABELS[btn.action] || btn.action;
        bg.appendChild(btnTip);
        bg.addEventListener('click', (e) => {
            e.stopPropagation();
            if (btn.action === 'edit' && callbacks.onEditNode) callbacks.onEditNode(node);
            if (btn.action === 'delete' && callbacks.onDeleteNode) callbacks.onDeleteNode(node);
            if (btn.action === 'startstop' && callbacks.onStartStopNode) callbacks.onStartStopNode(node);
            if (btn.action === 'test' && callbacks.onTestNode) callbacks.onTestNode(node);
            if (btn.action === 'notify' && callbacks.onNotificationTest) callbacks.onNotificationTest(node);
            if (btn.action === 'clone' && callbacks.onCloneNode) callbacks.onCloneNode(node);
            if (btn.action === 'activate' && callbacks.onActivatePreset) callbacks.onActivatePreset(node);
            if (btn.action === 'color') _openNodeColorPicker(node, e);
        });
        overlay.appendChild(bg);
    });

    return overlay;
}

/**
 * Patch a node's running state in-place without replacing the element.
 * Updates the start/stop button icon/class, running dot, and node CSS class.
 */
export function patchNodeRunning(group: SVGGElement, node: GraphNode): void {
    const el = group.querySelector(`.graph-node[data-id="${node.id}"]`);
    if (!el) return;

    // Toggle running class
    el.classList.toggle('running', !!node.running);

    // Update or add/remove running dot
    let dot = el.querySelector('.graph-node-running-dot');
    if (node.running && !dot) {
        dot = svgEl('circle', {
            class: 'graph-node-running-dot',
            cx: node.width - 14, cy: 14, r: 4,
        });
        // Insert before the overlay group
        const overlay = el.querySelector('.graph-node-overlay');
        if (overlay) el.insertBefore(dot, overlay);
        else el.appendChild(dot);
    } else if (!node.running && dot) {
        dot.remove();
    }

    // Update start/stop button in overlay
    const overlayBtns = el.querySelectorAll('.graph-node-overlay-btn');
    for (const btn of overlayBtns) {
        const txt = btn.querySelector('text');
        if (!txt) continue;
        const content = txt.textContent;
        // Match play ▶ or stop ■ icons
        if (content === '\u25B6' || content === '\u25A0') {
            txt.textContent = node.running ? '\u25A0' : '\u25B6';
            btn.classList.toggle('success', !node.running);
            btn.classList.toggle('danger', !!node.running);
            break;
        }
    }
}

/**
 * Highlight a single node (add class, scroll to).
 */
export function highlightNode(group: SVGGElement, nodeId: string | null, cls: string = 'search-match'): Element | null {
    // Remove existing highlights
    group.querySelectorAll(`.graph-node.${cls}`).forEach(n => n.classList.remove(cls));
    const node = group.querySelector(`.graph-node[data-id="${nodeId}"]`);
    if (node) node.classList.add(cls);
    return node;
}

/**
 * Mark orphan nodes (no incoming or outgoing edges).
 */
export function markOrphans(group: SVGGElement, nodeMap: Map<string, GraphNode>, edges: GraphEdge[]): void {
    const connected = new Set();
    for (const e of edges) {
        connected.add(e.from);
        connected.add(e.to);
    }
    for (const node of nodeMap.values()) {
        const el = group.querySelector(`.graph-node[data-id="${node.id}"]`);
        if (!el) continue;
        el.classList.toggle('orphan', !connected.has(node.id));
    }
}

/**
 * Update selection state on nodes.
 */
export function updateSelection(group: SVGGElement, selectedIds: Set<string>): void {
    group.querySelectorAll('.graph-node').forEach(n => {
        n.classList.toggle('selected', selectedIds.has(n.getAttribute('data-id') ?? ''));
    });
}
