import { getLabel, getShortLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getFirstName } from './nodes.js';
import { buildSwitchUplinks, buildLocationTree, buildNodeIndex, findLocationForNode, findEffectiveSwitch } from './topology.js';
import { formatUniverse } from './format.js';
import { createNodeElement, renderLocation } from './components.js';
import { updateErrorPanel, updateBroadcastStats } from './ui.js';
import { renderTable } from './table.js';
import { showFlowView } from './flow.js';
import {
    nodeElements, locationElements,
    setUsedNodeIds, setUsedLocationIds, setPortErrors,
    setTableData, setFlowViewData, currentView,
    resetAnonCounter
} from './state.js';

export function render(data, config) {
    resetAnonCounter();

    const nodes = data.nodes || [];
    const links = data.links || [];

    setPortErrors(data.errors || []);
    const unreachableNodeIds = new Set(nodes.filter(n => n.unreachable).map(n => n.id));
    const errorNodeIds = new Set((data.errors || []).filter(e => e.type !== 'unreachable').map(e => e.node_id));

    const locationTree = buildLocationTree(config.locations || [], null);
    const nodeIndex = new Map();
    buildNodeIndex(locationTree, nodeIndex);

    const nodesByTypeId = new Map();
    nodes.forEach(node => {
        nodesByTypeId.set(node.id, node);
    });

    const nodeLocations = new Map();
    const assignedNodes = new Map();
    const unassignedNodes = [];

    nodes.forEach(node => {
        const loc = findLocationForNode(node, nodeIndex);
        if (loc) {
            nodeLocations.set(node.id, loc);
            if (!assignedNodes.has(loc)) {
                assignedNodes.set(loc, []);
            }
            assignedNodes.get(loc).push(node);
        } else {
            unassignedNodes.push(node);
        }
    });

    const switchConnections = new Map();
    const switchLinks = [];
    const allSwitches = nodes.filter(n => isSwitch(n));

    links.forEach(link => {
        const nodeA = nodesByTypeId.get(link.node_a_id);
        const nodeB = nodesByTypeId.get(link.node_b_id);
        if (!nodeA || !nodeB) return;

        const aIsSwitch = isSwitch(nodeA);
        const bIsSwitch = isSwitch(nodeB);

        if (aIsSwitch && bIsSwitch) {
            switchLinks.push({
                switchA: nodeA,
                switchB: nodeB,
                portA: link.interface_a || '?',
                portB: link.interface_b || '?',
                speedA: getInterfaceSpeed(nodeA, link.interface_a),
                speedB: getInterfaceSpeed(nodeB, link.interface_b),
                errorsA: getInterfaceErrors(nodeA, link.interface_a),
                errorsB: getInterfaceErrors(nodeB, link.interface_b),
                ratesA: getInterfaceRates(nodeA, link.interface_a),
                ratesB: getInterfaceRates(nodeB, link.interface_b)
            });
        } else if (aIsSwitch && !bIsSwitch) {
            const nodeLoc = nodeLocations.get(nodeB.id);
            const effectiveSwitch = findEffectiveSwitch(nodeLoc, assignedNodes);
            const isLocalSwitch = effectiveSwitch && effectiveSwitch.id === nodeA.id;
            switchConnections.set(nodeB.id, {
                port: link.interface_a || '?',
                switchName: getLabel(nodeA),
                showSwitchName: !isLocalSwitch,
                external: effectiveSwitch && !isLocalSwitch,
                speed: getInterfaceSpeed(nodeA, link.interface_a),
                errors: getInterfaceErrors(nodeA, link.interface_a),
                rates: getInterfaceRates(nodeA, link.interface_a)
            });
        } else if (bIsSwitch && !aIsSwitch) {
            const nodeLoc = nodeLocations.get(nodeA.id);
            const effectiveSwitch = findEffectiveSwitch(nodeLoc, assignedNodes);
            const isLocalSwitch = effectiveSwitch && effectiveSwitch.id === nodeB.id;
            switchConnections.set(nodeA.id, {
                port: link.interface_b || '?',
                switchName: getLabel(nodeB),
                showSwitchName: !isLocalSwitch,
                external: effectiveSwitch && !isLocalSwitch,
                speed: getInterfaceSpeed(nodeB, link.interface_b),
                errors: getInterfaceErrors(nodeB, link.interface_b),
                rates: getInterfaceRates(nodeB, link.interface_b)
            });
        }
    });

    const danteNodes = new Map();

    const formatDanteChannel = (ch) => {
        let str = ch.tx_channel + ' → ' + String(ch.rx_channel).padStart(2, '0');
        if (ch.type) str += ' [' + ch.type + ']';
        if (ch.status === 'no-source') str += ' ⚠';
        return str;
    };

    nodes.forEach(node => {
        const nodeId = node.id;
        const danteTx = node.dante_flows?.tx || [];
        const danteRx = node.dante_flows?.rx || [];

        if (danteTx.length === 0 && danteRx.length === 0) return;

        const txEntries = danteTx.map(peer => {
            const peerNode = nodesByTypeId.get(peer.node_id);
            const peerName = peerNode ? getFirstName(peerNode) : '??';
            const channels = (peer.channels || []).map(formatDanteChannel);
            const channelSummary = channels.length > 0 ? '\n  ' + channels.join('\n  ') : '';
            return { text: peerName + channelSummary, peerName };
        });

        const rxEntries = danteRx.map(peer => {
            const peerNode = nodesByTypeId.get(peer.node_id);
            const peerName = peerNode ? getFirstName(peerNode) : '??';
            const channels = (peer.channels || []).map(formatDanteChannel);
            const channelSummary = channels.length > 0 ? '\n  ' + channels.join('\n  ') : '';
            return { text: peerName + channelSummary, peerName };
        });

        txEntries.sort((a, b) => a.text.split('\n')[0].localeCompare(b.text.split('\n')[0]));
        rxEntries.sort((a, b) => a.text.split('\n')[0].localeCompare(b.text.split('\n')[0]));

        danteNodes.set(nodeId, {
            isTx: danteTx.length > 0,
            isRx: danteRx.length > 0,
            txTo: txEntries.map(e => e.text),
            txToPeerNames: txEntries.map(e => e.peerName),
            rxFrom: rxEntries.map(e => e.text),
            rxFromPeerNames: rxEntries.map(e => e.peerName),
            nodeName: getFirstName(node)
        });
    });

    const artnetNodes = new Map();

    const universeInputs = new Map();
    const universeOutputs = new Map();

    nodes.forEach(node => {
        const name = getFirstName(node);
        (node.artnet_inputs || []).forEach(u => {
            if (!universeInputs.has(u)) universeInputs.set(u, []);
            universeInputs.get(u).push(name);
        });
        (node.artnet_outputs || []).forEach(u => {
            if (!universeOutputs.has(u)) universeOutputs.set(u, []);
            universeOutputs.get(u).push(name);
        });
    });

    const collapseNames = (names) => {
        const counts = {};
        names.forEach(n => counts[n] = (counts[n] || 0) + 1);
        return Object.entries(counts).map(([name, count]) => count > 1 ? name + ' x' + count : name);
    };

    nodes.forEach(node => {
        const nodeId = node.id;
        const artnetInputs = node.artnet_inputs || [];
        const artnetOutputs = node.artnet_outputs || [];

        if (artnetInputs.length === 0 && artnetOutputs.length === 0) return;

        const sortedInputs = artnetInputs.slice().sort((a, b) => a - b);
        const sortedOutputs = artnetOutputs.slice().sort((a, b) => a - b);

        const inputs = sortedInputs.map(u => {
            const sources = collapseNames(universeOutputs.get(u) || []);
            const uniStr = formatUniverse(u);
            if (sources.length > 0) {
                return { display: sources[0] + ' [' + uniStr + ']', firstTarget: sources[0], universe: u };
            }
            return { display: uniStr, firstTarget: null, universe: u };
        });
        const outputs = sortedOutputs.map(u => {
            const dests = collapseNames(universeInputs.get(u) || []);
            const uniStr = formatUniverse(u);
            if (dests.length > 0) {
                return { display: dests[0] + ' [' + uniStr + ']', firstTarget: dests[0], universe: u };
            }
            return { display: uniStr, firstTarget: null, universe: u };
        });

        artnetNodes.set(nodeId, {
            isOut: outputs.length > 0,
            isIn: inputs.length > 0,
            outputs: outputs,
            inputs: inputs
        });
    });

    const sacnNodes = new Map();

    const sacnUniverseInputs = new Map();
    const sacnUniverseOutputs = new Map();

    function getSacnInputs(node) {
        const inputs = [];
        (node.multicast_groups || []).forEach(g => {
            if (typeof g === 'string' && g.startsWith('sacn:')) {
                const u = parseInt(g.substring(5), 10);
                if (!isNaN(u)) inputs.push(u);
            }
        });
        (node.sacn_unicast_inputs || []).forEach(u => {
            if (!inputs.includes(u)) inputs.push(u);
        });
        return inputs;
    }

    nodes.forEach(node => {
        const name = getFirstName(node);
        getSacnInputs(node).forEach(u => {
            if (!sacnUniverseInputs.has(u)) sacnUniverseInputs.set(u, []);
            sacnUniverseInputs.get(u).push(name);
        });
        (node.sacn_outputs || []).forEach(u => {
            if (!sacnUniverseOutputs.has(u)) sacnUniverseOutputs.set(u, []);
            sacnUniverseOutputs.get(u).push(name);
        });
    });

    const sacnCollapseNames = (names) => {
        const counts = {};
        names.forEach(n => counts[n] = (counts[n] || 0) + 1);
        return Object.entries(counts).map(([name, count]) => count > 1 ? name + ' x' + count : name);
    };

    nodes.forEach(node => {
        const nodeId = node.id;
        const sacnInputs = getSacnInputs(node);
        const sacnOutputs = node.sacn_outputs || [];

        if (sacnInputs.length === 0 && sacnOutputs.length === 0) return;

        const sortedSacnInputs = sacnInputs.slice().sort((a, b) => a - b);
        const sortedSacnOutputs = sacnOutputs.slice().sort((a, b) => a - b);

        const inputs = sortedSacnInputs.map(u => {
            const sources = sacnCollapseNames(sacnUniverseOutputs.get(u) || []);
            if (sources.length > 0) {
                return { display: sources[0] + ' [' + u + ']', firstTarget: sources[0], universe: u };
            }
            return { display: String(u), firstTarget: null, universe: u };
        });
        const outputs = sortedSacnOutputs.map(u => {
            const dests = sacnCollapseNames(sacnUniverseInputs.get(u) || []);
            if (dests.length > 0) {
                return { display: dests[0] + ' [' + u + ']', firstTarget: dests[0], universe: u };
            }
            return { display: String(u), firstTarget: null, universe: u };
        });

        sacnNodes.set(nodeId, {
            isOut: outputs.length > 0,
            isIn: inputs.length > 0,
            outputs: outputs,
            inputs: inputs
        });
    });

    const switchUplinks = buildSwitchUplinks(allSwitches, switchLinks);

    const container = document.getElementById('container');
    const usedNodeIdsSet = new Set();
    const usedLocationIdsSet = new Set();

    locationTree.forEach(loc => {
        const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks, danteNodes, artnetNodes, sacnNodes, errorNodeIds, unreachableNodeIds, usedNodeIdsSet, usedLocationIdsSet);
        if (el && el.parentNode !== container) container.appendChild(el);
    });

    let unassignedLoc = locationElements.get('__unassigned__');
    if (unassignedNodes.length > 0) {
        if (!unassignedLoc) {
            unassignedLoc = document.createElement('div');
            unassignedLoc.className = 'location top-level';
            const nameEl = document.createElement('div');
            nameEl.className = 'location-name';
            nameEl.textContent = 'Unassigned';
            unassignedLoc.appendChild(nameEl);
            locationElements.set('__unassigned__', unassignedLoc);
        }

        const switches = unassignedNodes.filter(n => isSwitch(n));
        const nonSwitches = unassignedNodes.filter(n => !isSwitch(n));

        let switchRow = unassignedLoc.querySelector(':scope > .node-row.switch-row');
        if (switches.length > 0) {
            if (!switchRow) {
                switchRow = document.createElement('div');
                switchRow.className = 'node-row switch-row';
                unassignedLoc.appendChild(switchRow);
            }
            const currentIds = new Set(switches.map(n => n.id));
            Array.from(switchRow.children).forEach(ch => {
                if (!currentIds.has(ch.dataset.id)) ch.remove();
            });
            switches.forEach(node => {
                usedNodeIdsSet.add(node.id);
                const uplink = switchUplinks.get(node.id);
                const danteInfo = danteNodes.get(node.id);
                const artnetInfo = artnetNodes.get(node.id);
                const sacnInfo = sacnNodes.get(node.id);
                const hasError = errorNodeIds.has(node.id);
                const isUnreachable = unreachableNodeIds.has(node.id);
                const el = createNodeElement(node, null, null, uplink, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
                if (el.parentNode !== switchRow) switchRow.appendChild(el);
            });
        } else if (switchRow) {
            switchRow.remove();
        }

        let nodeRow = unassignedLoc.querySelector(':scope > .node-row:not(.switch-row)');
        if (nonSwitches.length > 0) {
            if (!nodeRow) {
                nodeRow = document.createElement('div');
                nodeRow.className = 'node-row';
                unassignedLoc.appendChild(nodeRow);
            }
            const currentIds = new Set(nonSwitches.map(n => n.id));
            Array.from(nodeRow.children).forEach(ch => {
                if (!currentIds.has(ch.dataset.id)) ch.remove();
            });
            nonSwitches.forEach(node => {
                usedNodeIdsSet.add(node.id);
                const conn = switchConnections.get(node.id);
                const danteInfo = danteNodes.get(node.id);
                const artnetInfo = artnetNodes.get(node.id);
                const sacnInfo = sacnNodes.get(node.id);
                const hasError = errorNodeIds.has(node.id);
                const isUnreachable = unreachableNodeIds.has(node.id);
                const el = createNodeElement(node, conn, null, null, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
                if (el.parentNode !== nodeRow) nodeRow.appendChild(el);
            });
        } else if (nodeRow) {
            nodeRow.remove();
        }

        if (unassignedLoc.parentNode !== container) container.appendChild(unassignedLoc);
        usedLocationIdsSet.add('__unassigned__');
    } else if (unassignedLoc) {
        unassignedLoc.remove();
    }

    setUsedNodeIds(usedNodeIdsSet);
    setUsedLocationIds(usedLocationIdsSet);

    locationElements.forEach((el, id) => {
        if (!usedLocationIdsSet.has(id) && el.parentNode) {
            el.remove();
        }
    });

    updateErrorPanel();
    updateBroadcastStats(data.broadcast_stats);

    setTableData(data);
    setFlowViewData(data);
    if (currentView === 'table') {
        renderTable();
    }
    const hash = window.location.hash;
    if (hash.startsWith('#flow/')) {
        showFlowView(hash.slice(6));
    }
}
