/**
 * Graph layout via ELK.js — converts entity data into positioned nodes/edges.
 */

import ELK from 'elkjs/lib/elk.bundled.js';
import { bindableSourceId } from '../types.ts';

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

interface LayoutNode {
    id: string;
    kind: string;
    name: string;
    subtype: string;
    tags: string[];
    running?: boolean;
    x?: number;
    y?: number;
    width?: number;
    height?: number;
}

interface LayoutEdge {
    from: string;
    to: string;
    field: string;
    label: string;
    type: string;
    editable: boolean;
}

interface LayoutResult {
    nodes: Map<string, LayoutNode>;
    edges: (LayoutEdge & { points: { x: number; y: number }[] | null; fromNode: LayoutNode; toNode: LayoutNode })[];
    bounds: { x: number; y: number; width: number; height: number };
}

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

interface EntitiesInput {
    devices?: any[];
    captureTemplates?: any[];
    ppTemplates?: any[];
    audioTemplates?: any[];
    patternTemplates?: any[];
    syncClocks?: any[];
    pictureSources?: any[];
    audioSources?: any[];
    valueSources?: any[];
    colorStripSources?: any[];
    outputTargets?: any[];
    scenePresets?: any[];
    automations?: any[];
    csptTemplates?: any[];
}

const NODE_WIDTH = 190;
const NODE_HEIGHT = 56;

const ELK_OPTIONS = {
    'elk.algorithm': 'layered',
    'elk.direction': 'RIGHT',
    'elk.spacing.nodeNode': '40',
    'elk.layered.spacing.nodeNodeBetweenLayers': '80',
    'elk.layered.spacing.edgeNodeBetweenLayers': '30',
    'elk.spacing.edgeEdge': '15',
    'elk.spacing.edgeNode': '20',
    'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
    'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
    'elk.edgeRouting': 'SPLINES',
    'elk.layered.mergeEdges': 'true',
};

/**
 * Build ELK graph from entity model and run layout.
 * @param {Object} entities - { devices, captureTemplates, pictureSources, ... }
 * @returns {Promise<{nodes: Map, edges: Array, bounds: {x,y,width,height}}>}
 */
export async function computeLayout(entities: EntitiesInput): Promise<LayoutResult> {
    const elk = new ELK();
    const { nodes: nodeList, edges: edgeList } = buildGraph(entities);

    const elkGraph = {
        id: 'root',
        layoutOptions: ELK_OPTIONS,
        children: nodeList.map(n => ({
            id: n.id,
            width: NODE_WIDTH,
            height: NODE_HEIGHT,
        })),
        edges: edgeList.map((e, i) => ({
            id: `e${i}`,
            sources: [e.from],
            targets: [e.to],
        })),
    };

    const layout = await elk.layout(elkGraph);

    const nodeMap = new Map();
    const nodeById = new Map(nodeList.map(n => [n.id, n]));
    for (const child of layout.children!) {
        const src = nodeById.get(child.id);
        if (src) {
            nodeMap.set(child.id, {
                ...src,
                x: child.x,
                y: child.y,
                width: child.width,
                height: child.height,
            });
        }
    }

    // Build edge paths from layout
    const edges: any[] = [];
    for (let i = 0; i < edgeList.length; i++) {
        const layoutEdge = layout.edges?.[i];
        const srcEdge = edgeList[i];
        const fromNode = nodeMap.get(srcEdge.from);
        const toNode = nodeMap.get(srcEdge.to);
        if (!fromNode || !toNode) continue;

        let points: any[] | null = null;
        if ((layoutEdge as any)?.sections?.[0]) {
            const sec = (layoutEdge as any).sections[0];
            points = [sec.startPoint, ...(sec.bendPoints || []), sec.endPoint];
        }

        edges.push({
            ...srcEdge,
            points,
            fromNode,
            toNode,
        });
    }

    // Compute bounding box
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const n of nodeMap.values()) {
        minX = Math.min(minX, n.x);
        minY = Math.min(minY, n.y);
        maxX = Math.max(maxX, n.x + n.width);
        maxY = Math.max(maxY, n.y + n.height);
    }

    const bounds = nodeMap.size > 0
        ? { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
        : { x: 0, y: 0, width: 400, height: 300 };

    return { nodes: nodeMap, edges, bounds };
}

/* ── Entity color mapping ── */

export const ENTITY_COLORS = {
    device:               '#78909C',
    capture_template:     '#8D6E63',
    picture_source:       '#42A5F5',
    pp_template:          '#AB47BC',
    audio_source:         '#EF5350',
    audio_template:       '#E57373',
    color_strip_source:   '#66BB6A',
    value_source:         '#FFA726',
    sync_clock:           '#26C6DA',
    output_target:        '#FFCA28',
    scene_preset:         '#CE93D8',
    automation:           '#A5D6A7',
    pattern_template:     '#BCAAA4',
    cspt:                 '#7E57C2',
};

export const ENTITY_LABELS = {
    device:               'Device',
    capture_template:     'Capture Template',
    picture_source:       'Picture Source',
    pp_template:          'PP Template',
    audio_source:         'Audio Source',
    audio_template:       'Audio Template',
    color_strip_source:   'Color Strip Source',
    value_source:         'Value Source',
    sync_clock:           'Sync Clock',
    output_target:        'Output Target',
    scene_preset:         'Scene Preset',
    automation:           'Automation',
    pattern_template:     'Pattern Template',
    cspt:                 'Strip Processing',
};

/* ── Edge type (for CSS class) ── */

function edgeType(fromKind: string, toKind: string, field: string): string {
    if (field === 'clock_id') return 'clock';
    if (fromKind === 'device') return 'device';
    if (fromKind === 'picture_source' || toKind === 'picture_source') return 'picture';
    if (fromKind === 'color_strip_source') return 'colorstrip';
    if (fromKind === 'value_source') return 'value';
    if (fromKind === 'audio_source' || fromKind === 'audio_template') return 'audio';
    if (fromKind === 'capture_template' || fromKind === 'pp_template' || fromKind === 'pattern_template') return 'template';
    if (fromKind === 'scene_preset') return 'scene';
    if (fromKind === 'cspt') return 'template';
    return 'default';
}

/* ── Graph builder ── */

function buildGraph(e: EntitiesInput): { nodes: LayoutNode[]; edges: LayoutEdge[] } {
    const nodes: LayoutNode[] = [];
    const edges: LayoutEdge[] = [];
    const nodeIds = new Set<string>();

    function addNode(id: string, kind: string, name: string, subtype: string, extra: Record<string, any> = {}): void {
        if (!id || nodeIds.has(id)) return;
        nodeIds.add(id);
        nodes.push({ id, kind, name: name || id, subtype: subtype || '', tags: extra.tags || [], ...extra });
    }

    function addEdge(from: string, to: string, field: string, label: string = ''): void {
        if (!from || !to || !nodeIds.has(from) || !nodeIds.has(to)) return;
        const type = edgeType(
            nodes.find(n => n.id === from)?.kind ?? '',
            nodes.find(n => n.id === to)?.kind ?? '',
            field
        );
        // Edges with dotted fields are nested (composite layers, zones, etc.) — not drag-editable
        const editable = !field.includes('.');
        edges.push({ from, to, field, label, type, editable });
    }

    // 1. Devices
    for (const d of e.devices || []) {
        addNode(d.id, 'device', d.name, d.device_type, { tags: d.tags });
    }

    // 2. Capture templates
    for (const t of e.captureTemplates || []) {
        addNode(t.id, 'capture_template', t.name, t.engine_type, { tags: t.tags });
    }

    // 3. PP templates
    for (const t of e.ppTemplates || []) {
        addNode(t.id, 'pp_template', t.name, '', { tags: t.tags });
    }

    // 4. Audio templates
    for (const t of e.audioTemplates || []) {
        addNode(t.id, 'audio_template', t.name, t.engine_type, { tags: t.tags });
    }

    // 5. Pattern templates
    for (const t of e.patternTemplates || []) {
        addNode(t.id, 'pattern_template', t.name, '', { tags: t.tags });
    }

    // 6. Sync clocks
    for (const c of e.syncClocks || []) {
        addNode(c.id, 'sync_clock', c.name, '', { running: c.is_running !== false, tags: c.tags });
    }

    // 7. Picture sources
    for (const s of e.pictureSources || []) {
        addNode(s.id, 'picture_source', s.name, s.stream_type, { tags: s.tags });
    }

    // 8. Audio sources
    for (const s of e.audioSources || []) {
        addNode(s.id, 'audio_source', s.name, s.source_type, { tags: s.tags });
    }

    // 9. Value sources
    for (const s of e.valueSources || []) {
        addNode(s.id, 'value_source', s.name, s.source_type, { tags: s.tags });
    }

    // 10. Color strip sources
    for (const s of e.colorStripSources || []) {
        addNode(s.id, 'color_strip_source', s.name, s.source_type, { tags: s.tags });
    }

    // 11. Output targets
    for (const t of e.outputTargets || []) {
        addNode(t.id, 'output_target', t.name, t.target_type, { running: t.running || false, tags: t.tags });
    }

    // 12. Scene presets
    for (const s of e.scenePresets || []) {
        addNode(s.id, 'scene_preset', s.name, '', { tags: s.tags });
    }

    // 13. Automations
    for (const a of e.automations || []) {
        addNode(a.id, 'automation', a.name, '', { running: a.enabled || false, tags: a.tags });
    }

    // 14. Color strip processing templates (CSPT)
    for (const t of e.csptTemplates || []) {
        addNode(t.id, 'cspt', t.name, '', { tags: t.tags });
    }

    // ── Edges ──

    // Picture source edges
    for (const s of e.pictureSources || []) {
        if (s.capture_template_id) addEdge(s.capture_template_id, s.id, 'capture_template_id');
        if (s.source_stream_id) addEdge(s.source_stream_id, s.id, 'source_stream_id');
        if (s.postprocessing_template_id) addEdge(s.postprocessing_template_id, s.id, 'postprocessing_template_id');
    }

    // Audio source edges
    for (const s of e.audioSources || []) {
        if (s.audio_template_id) addEdge(s.audio_template_id, s.id, 'audio_template_id');
        if (s.audio_source_id) addEdge(s.audio_source_id, s.id, 'audio_source_id');
    }

    // Value source edges
    for (const s of e.valueSources || []) {
        if (s.audio_source_id) addEdge(s.audio_source_id, s.id, 'audio_source_id');
        if (s.picture_source_id) addEdge(s.picture_source_id, s.id, 'picture_source_id');
    }

    // Color strip source edges
    for (const s of e.colorStripSources || []) {
        if (s.picture_source_id) addEdge(s.picture_source_id, s.id, 'picture_source_id');
        if (s.audio_source_id) addEdge(s.audio_source_id, s.id, 'audio_source_id');
        if (s.clock_id) addEdge(s.clock_id, s.id, 'clock_id');

        // Processed type: input source and processing template edges
        if (s.input_source_id) addEdge(s.input_source_id, s.id, 'input_source_id');
        if (s.processing_template_id) addEdge(s.processing_template_id, s.id, 'processing_template_id');

        // Composite layers
        if (s.layers) {
            for (const layer of s.layers) {
                if (layer.source_id) addEdge(layer.source_id, s.id, 'layer.source_id');
                if (layer.brightness_source_id) addEdge(layer.brightness_source_id, s.id, 'layer.brightness_source_id');
            }
        }

        // Mapped zones
        if (s.zones) {
            for (const zone of s.zones) {
                if (zone.source_id) addEdge(zone.source_id, s.id, 'zone.source_id');
            }
        }

        // Advanced picture calibration lines
        if (s.calibration?.lines) {
            for (const line of s.calibration.lines) {
                if (line.picture_source_id) addEdge(line.picture_source_id, s.id, 'calibration.picture_source_id');
            }
        }

        // BindableFloat value source edges
        for (const prop of ['smoothing', 'sensitivity', 'intensity', 'scale', 'speed',
            'wind_strength', 'temperature_influence', 'sound_volume', 'timeout', 'brightness'] as const) {
            const vsId = bindableSourceId((s as any)[prop]);
            if (vsId) addEdge(vsId, s.id, `${prop}.source_id`);
        }
        // BindableColor value source edges
        for (const prop of ['color', 'color_peak', 'fallback_color', 'default_color'] as const) {
            const raw = (s as any)[prop];
            if (raw && typeof raw === 'object' && !Array.isArray(raw) && raw.source_id) {
                addEdge(raw.source_id, s.id, `${prop}.source_id`);
            }
        }
    }

    // Output target edges
    for (const t of e.outputTargets || []) {
        if (t.device_id) addEdge(t.device_id, t.id, 'device_id');
        if (t.color_strip_source_id) addEdge(t.color_strip_source_id, t.id, 'color_strip_source_id');
        const bvsId = bindableSourceId(t.brightness);
        if (bvsId) addEdge(bvsId, t.id, 'brightness.source_id');
        const transVsId = bindableSourceId(t.transition);
        if (transVsId) addEdge(transVsId, t.id, 'transition.source_id');
        if (t.picture_source_id) addEdge(t.picture_source_id, t.id, 'picture_source_id');
        // KC target settings
        if (t.settings) {
            if (t.settings.pattern_template_id) addEdge(t.settings.pattern_template_id, t.id, 'settings.pattern_template_id');
            const settingsBvsId = bindableSourceId(t.settings?.brightness);
            if (settingsBvsId) addEdge(settingsBvsId, t.id, 'settings.brightness.source_id');
        }
    }

    // Scene preset edges
    for (const s of e.scenePresets || []) {
        if (s.targets) {
            for (const snap of s.targets) {
                if (snap.target_id) addEdge(snap.target_id, s.id, 'target_id');
            }
        }
    }

    // Automation edges
    for (const a of e.automations || []) {
        if (a.scene_preset_id) addEdge(a.scene_preset_id, a.id, 'scene_preset_id');
        if (a.deactivation_scene_preset_id) addEdge(a.deactivation_scene_preset_id, a.id, 'deactivation_scene_preset_id');
    }

    // Composite layer → CSPT edges
    for (const s of e.colorStripSources || []) {
        if (s.layers) {
            for (const layer of s.layers) {
                if (layer.processing_template_id) addEdge(layer.processing_template_id, s.id, 'layer.processing_template_id');
            }
        }
    }

    // Device → CSPT default template edges
    for (const d of e.devices || []) {
        if (d.default_css_processing_template_id) addEdge(d.default_css_processing_template_id, d.id, 'default_css_processing_template_id');
    }

    return { nodes, edges };
}

/* ── Port computation ── */

/** Canonical ordering of port types (top → bottom on the node). */
const PORT_TYPE_ORDER = ['template', 'picture', 'colorstrip', 'value', 'audio', 'clock', 'scene', 'device', 'default'];

/**
 * Compute input/output port positions on every node from the edge list.
 * Mutates edges (adds fromPortY, toPortY) and nodes (adds inputPorts, outputPorts).
 */
export function computePorts(nodeMap: Map<string, LayoutNode & { inputPorts?: PortSet; outputPorts?: PortSet; height: number }>, edges: (LayoutEdge & { fromPortY?: number; toPortY?: number })[]): void {
    // Collect which port types each node needs (keyed by edge type)
    const inputTypes  = new Map();  // nodeId → Set<edgeType>
    const outputTypes = new Map();  // nodeId → Set<edgeType>

    for (const edge of edges) {
        if (!inputTypes.has(edge.to)) inputTypes.set(edge.to, new Set());
        inputTypes.get(edge.to).add(edge.type);

        if (!outputTypes.has(edge.from)) outputTypes.set(edge.from, new Set());
        outputTypes.get(edge.from).add(edge.type);
    }

    // Sort port types and assign vertical positions
    function assignPorts(typeSet: Set<string>, height: number): PortSet {
        const types = [...typeSet].sort((a, b) => {
            const ai = PORT_TYPE_ORDER.indexOf(a);
            const bi = PORT_TYPE_ORDER.indexOf(b);
            return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi);
        });
        const ports = {};
        const n = types.length;
        types.forEach((t, i) => {
            ports[t] = height * (i + 1) / (n + 1);
        });
        return { types, ports };
    }

    // Build port maps on nodes
    for (const [id, node] of nodeMap) {
        const inSet = inputTypes.get(id) || new Set();
        const outSet = outputTypes.get(id) || new Set();
        node.inputPorts  = assignPorts(inSet, node.height);
        node.outputPorts = assignPorts(outSet, node.height);
    }

    // Annotate edges with port Y offsets
    for (const edge of edges) {
        const fromNode = nodeMap.get(edge.from);
        const toNode = nodeMap.get(edge.to);
        if (fromNode?.outputPorts?.ports) {
            edge.fromPortY = fromNode.outputPorts.ports[edge.type] ?? fromNode.height / 2;
        } else {
            edge.fromPortY = fromNode ? fromNode.height / 2 : 0;
        }
        if (toNode?.inputPorts?.ports) {
            edge.toPortY = toNode.inputPorts.ports[edge.type] ?? toNode.height / 2;
        } else {
            edge.toPortY = toNode ? toNode.height / 2 : 0;
        }
    }
}

export { NODE_WIDTH, NODE_HEIGHT };
