{% extends 'generic/_base.html' %}
{% load helpers %}
{% load static %}

{% block title %}Rack-Ansicht{% endblock %}

{% block head %}
  <link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
  <link rel="stylesheet" href="{% static 'netbox_topology_views/css/rack_elevation.css' %}">
{% endblock %}

{% block content %}
<div class="card mb-3">
  <div class="card-header"><strong>Racks auswählen</strong></div>
  <div class="card-body">
    <form method="get">
      <div class="row g-3">
        {% for field in filter_form %}
          <div class="col-md-3">
            <label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
            {{ field }}
            {% if field.help_text %}<div class="form-hint">{{ field.help_text }}</div>{% endif %}
            {% for error in field.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
          </div>
        {% endfor %}
      </div>
      <div class="mt-3">
        <button class="btn btn-primary" type="submit"><i class="mdi mdi-magnify"></i> Anzeigen</button>
        <a class="btn btn-outline-secondary" href="{% url 'plugins:netbox_topology_views:rack_elevation' %}">Zurücksetzen</a>
      </div>
    </form>
  </div>
</div>

{% if racks %}
<div class="d-flex justify-content-end align-items-center gap-2 mb-3 flex-wrap">
  <strong class="me-2">Alle Racks exportieren:</strong>
  <button class="btn btn-sm btn-outline-primary rack-export" data-format="svg" data-scope="all">SVG</button>
  <button class="btn btn-sm btn-outline-primary rack-export" data-format="png" data-scope="all">PNG</button>
  <button class="btn btn-sm btn-outline-primary rack-export" data-format="drawio" data-scope="all">draw.io</button>
</div>
<div class="rack-elevation-grid">
  {% for rack in racks %}
  <section class="rack-card" data-rack-id="{{ rack.pk }}" data-rack-units="{{ rack.u_height }}">
    <header>
      <div><a href="{{ rack.get_absolute_url }}"><strong>{{ rack.name }}</strong></a><br>
      <small>{{ rack.site }}{% if rack.location %} · {{ rack.location }}{% endif %} · {{ rack.u_height }}U</small></div>
      <div class="btn-group" aria-label="{{ rack.name }} exportieren">
        <button class="btn btn-sm btn-outline-secondary rack-export" data-format="svg" data-scope="{{ rack.pk }}">SVG</button>
        <button class="btn btn-sm btn-outline-secondary rack-export" data-format="png" data-scope="{{ rack.pk }}">PNG</button>
        <button class="btn btn-sm btn-outline-secondary rack-export" data-format="drawio" data-scope="{{ rack.pk }}">draw.io</button>
      </div>
    </header>
    <div class="rack-faces">
      {% for face, face_label in faces %}
      <div class="rack-face">
        <div class="rack-face-title">{{ face_label }}</div>
        <div class="rack-frame" style="--rack-units: {{ rack.u_height }}">
          <div class="rack-unit-labels">
            {% for unit in rack.elevation_unit_labels %}<span>{{ unit }}</span>{% endfor %}
          </div>
          <div class="rack-device-area">
            {% for device in rack.elevation_devices %}
              {% if device.elevation_face == face %}
              <a class="rack-device" href="{{ device.get_absolute_url }}"
                 data-face="{{ face }}" data-device-name="{{ device.name|default:device.device_type.model }}"
                 data-device-type="{{ device.device_type }}" data-position="{{ device.position|default:1 }}"
                 data-device-height="{{ device.device_type.u_height|default:1 }}" data-device-color="#{{ device.role.color|default:'1685fc' }}"
                 style="bottom: {{ device.elevation_bottom }}%; height: {{ device.elevation_height }}%; border-color: #{{ device.role.color }}"
                 title="{{ device.name }} · {{ device.device_type }} · U{{ device.position }}">
                <span>{{ device.name|default:device.device_type.model }}</span>
                <small>U{{ device.position }} · {{ device.device_type }}</small>
              </a>
              {% endif %}
            {% endfor %}
          </div>
        </div>
      </div>
      {% endfor %}
    </div>
  </section>
  {% endfor %}
</div>

<div id="rack-topology-card" class="card mt-4">
  <div class="card-header d-flex justify-content-between align-items-center">
    <strong>Kabeltopologie</strong>
    <div class="d-flex gap-2">
      <div class="btn-group">
        <button class="btn btn-sm btn-outline-primary topology-export" data-format="svg">SVG</button>
        <button class="btn btn-sm btn-outline-primary topology-export" data-format="png">PNG</button>
        <button class="btn btn-sm btn-outline-primary topology-export" data-format="drawio">draw.io</button>
      </div>
      <div class="btn-group" aria-label="Topologie zoomen">
        <button id="rack-topology-zoom-out" class="btn btn-sm btn-outline-secondary" type="button" title="Verkleinern"><i class="mdi mdi-minus"></i></button>
        <button id="rack-topology-zoom-reset" class="btn btn-sm btn-outline-secondary" type="button" title="Zoom zurücksetzen"><span id="rack-topology-zoom-label">100%</span></button>
        <button id="rack-topology-zoom-in" class="btn btn-sm btn-outline-secondary" type="button" title="Vergrößern"><i class="mdi mdi-plus"></i></button>
      </div>
      <button id="rack-topology-reset" class="btn btn-sm btn-outline-secondary" type="button">
        <i class="mdi mdi-restore"></i> Positionen zurücksetzen
      </button>
      <button id="rack-topology-fullscreen" class="btn btn-sm btn-outline-secondary" type="button">
        <i class="mdi mdi-fullscreen"></i> Vollbild
      </button>
    </div>
  </div>
  <div class="card-body">
    <div id="rack-topology" class="rack-topology" aria-label="Topologie der ausgewählten Racks">
      <svg class="rack-topology-lines" aria-hidden="true"></svg>
      <div class="rack-topology-columns"></div>
      <div class="rack-topology-tooltip" role="tooltip"></div>
    </div>
  </div>
</div>
{{ rack_topology|json_script:"rack-topology-data" }}
{{ rack_export|json_script:"rack-export-data" }}

<div class="card mt-4">
  <div class="card-header"><strong>Verkabelung der angezeigten Racks</strong> <span class="badge bg-secondary">{{ cables|length }}</span></div>
  <div class="table-responsive">
    <table class="table table-hover mb-0">
      <thead><tr><th>Kabel</th><th>A-Seite</th><th>B-Seite</th><th>Typ</th><th>Status</th></tr></thead>
      <tbody>
      {% for row in cables %}
        <tr>
          <td><a href="{{ row.cable.get_absolute_url }}">{{ row.cable.label|default:row.cable }}</a></td>
          <td>{% if row.a %}<strong>{{ row.a.device_name }}</strong><br><small>{{ row.a.name }}</small>{% else %}—{% endif %}</td>
          <td>{% if row.b %}<strong>{{ row.b.device_name }}</strong><br><small>{{ row.b.name }}</small>{% else %}—{% endif %}</td>
          <td>{{ row.cable.type|default:'—' }}</td><td>{{ row.cable.get_status_display }}</td>
        </tr>
      {% empty %}<tr><td colspan="5" class="text-muted text-center py-4">Keine Verkabelung gefunden.</td></tr>{% endfor %}
      </tbody>
    </table>
  </div>
</div>
{% elif has_selection %}
<div class="alert alert-warning">Für diese Auswahl wurden keine belegten Racks gefunden. Aktiviere „Leere Racks einschließen“, um auch leere Racks zu sehen.</div>
{% else %}
<div class="alert alert-info">Wähle ein oder mehrere Racks oder grenze die Ansicht über Standort und Location ein.</div>
{% endif %}
{% endblock %}

{% block javascript %}
<script>
(() => {
  const root = document.getElementById('rack-topology');
  const source = document.getElementById('rack-topology-data');
  if (!root || !source) return;
  const data = JSON.parse(source.textContent);
  const isWirelessEdge = edge => edge.kind === 'wireless' || edge.kind === 'wireless-lan';
  const rackData = JSON.parse(document.getElementById('rack-export-data').textContent);
  const columns = root.querySelector('.rack-topology-columns');
  const svg = root.querySelector('svg');
  const tooltip = root.querySelector('.rack-topology-tooltip');
  const resetButton = document.getElementById('rack-topology-reset');
  const fullscreenButton = document.getElementById('rack-topology-fullscreen');
  const topologyCard = document.getElementById('rack-topology-card');
  const zoomInButton = document.getElementById('rack-topology-zoom-in');
  const zoomOutButton = document.getElementById('rack-topology-zoom-out');
  const zoomResetButton = document.getElementById('rack-topology-zoom-reset');
  const zoomLabel = document.getElementById('rack-topology-zoom-label');
  let topologyZoom = 1;
  const activeNetBoxTheme = () => document.body.dataset.bsTheme ||
    document.documentElement.dataset.bsTheme ||
    document.documentElement.dataset.netboxColorMode || 'light';
  const applyTopologyTheme = () => {
    root.style.setProperty(
      '--topology-highlight-color',
      activeNetBoxTheme() === 'dark' ? '#ffffff' : '#22c55e'
    );
  };
  applyTopologyTheme();
  const topologyThemeObserver = new MutationObserver(applyTopologyTheme);
  topologyThemeObserver.observe(document.body, {attributes: true, attributeFilter: ['data-bs-theme']});
  topologyThemeObserver.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ['data-bs-theme', 'data-netbox-color-mode'],
  });
  const storageKey = `netbox-rack-topology-v2:${window.location.pathname}${window.location.search}`;
  let storedPositions = {};
  try { storedPositions = JSON.parse(localStorage.getItem(storageKey) || '{}'); } catch (_) {}
  const groups = new Map();

  data.nodes.forEach(node => {
    const groupKey = node.rack_id ? `rack-${node.rack_id}` : `external-${node.rack_name || 'Extern'}`;
    if (!groups.has(groupKey)) groups.set(groupKey, {
      name: node.rack_name || 'Extern',
      rackUnits: node.rack_u_height || null,
      nodes: [],
    });
    groups.get(groupKey).nodes.push(node);
  });

  groups.forEach(group => {
    const column = document.createElement('section');
    column.className = 'rack-topology-column';
    const heading = document.createElement('h4');
    heading.textContent = group.name;
    column.appendChild(heading);
    const nodeContainer = document.createElement('div');
    nodeContainer.className = 'rack-topology-device-list';
    column.appendChild(nodeContainer);
    group.nodes.sort((a, b) => (b.position || 0) - (a.position || 0)).forEach(node => {
      const item = document.createElement(node.url ? 'a' : 'div');
      item.className = 'rack-topology-node';
      item.id = `topology-${node.node_id}`;
      item.style.setProperty('--node-color', `#${node.color}`);
      item.dataset.nodeId = node.node_id;
      if (node.url) item.href = node.url;
      const nameElement = document.createElement('strong');
      nameElement.textContent = node.device_name;
      const portElement = document.createElement('small');
      portElement.textContent = node.subtitle || node.name;
      item.append(nameElement, portElement);
      const saved = storedPositions[node.node_id];
      if (saved) item.style.transform = `translate(${saved.x}px, ${saved.y}px)`;
      nodeContainer.appendChild(item);
    });
    columns.appendChild(column);
  });

  const showTooltip = (html, anchor) => {
    tooltip.innerHTML = html;
    tooltip.classList.add('is-visible');
    const rootBounds = root.getBoundingClientRect();
    const anchorBounds = anchor.getBoundingClientRect();
    tooltip.style.left = `${anchorBounds.right - rootBounds.left + root.scrollLeft + 10}px`;
    tooltip.style.top = `${anchorBounds.top - rootBounds.top + root.scrollTop}px`;
  };
  const hideTooltip = () => tooltip.classList.remove('is-visible');
  const escapeHtml = value => {
    const element = document.createElement('span');
    element.textContent = value == null ? '—' : String(value);
    return element.innerHTML;
  };

  const drawEdges = () => {
    const bounds = root.getBoundingClientRect();
    let width = Math.max(root.clientWidth, columns.scrollWidth * topologyZoom + 32);
    let height = Math.max(root.clientHeight, columns.scrollHeight * topologyZoom + 32);
    svg.style.width = `${width}px`;
    svg.style.height = `${height}px`;
    svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
    svg.replaceChildren();
    data.edges.forEach(edge => {
      const from = document.getElementById(`topology-${edge.from}`);
      const to = document.getElementById(`topology-${edge.to}`);
      if (!from || !to) return;
      const a = from.getBoundingClientRect(), b = to.getBoundingClientRect();
      const x1 = a.left + a.width / 2 - bounds.left + root.scrollLeft;
      const y1 = a.top + a.height / 2 - bounds.top + root.scrollTop;
      const x2 = b.left + b.width / 2 - bounds.left + root.scrollLeft;
      const y2 = b.top + b.height / 2 - bounds.top + root.scrollTop;
      const bend = Math.max(30, Math.abs(x2 - x1) * .35);
      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      path.setAttribute('d', `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}`);
      path.setAttribute('stroke', edge.color);
      path.classList.add('rack-topology-edge');
      if (isWirelessEdge(edge)) path.classList.add('is-wireless');
      path.dataset.from = edge.from;
      path.dataset.to = edge.to;
      const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
      title.textContent = edge.label;
      path.appendChild(title);
      if (edge.url) path.addEventListener('click', () => window.location.href = edge.url);
      path.addEventListener('mouseenter', event => {
        path.classList.add('is-highlighted');
        showTooltip(
          `<strong>${isWirelessEdge(edge) ? 'WLAN · ' : ''}${escapeHtml(edge.label)}</strong>` +
          `<span>${escapeHtml(edge.from_device)} · ${escapeHtml(edge.from_port)}</span>` +
          `<span>↔ ${escapeHtml(edge.to_device)} · ${escapeHtml(edge.to_port)}</span>`,
          event.target
        );
      });
      path.addEventListener('mouseleave', () => {
        path.classList.remove('is-highlighted');
        hideTooltip();
      });
      svg.appendChild(path);
    });
  };

  const connectedEdges = nodeId => data.edges.filter(edge => edge.from === nodeId || edge.to === nodeId);
  root.querySelectorAll('.rack-topology-node').forEach(node => {
    const nodeId = node.dataset.nodeId;
    node.addEventListener('mouseenter', () => {
      const connections = connectedEdges(nodeId);
      svg.querySelectorAll('.rack-topology-edge').forEach(path => {
        path.classList.toggle('is-highlighted', path.dataset.from === nodeId || path.dataset.to === nodeId);
        path.classList.toggle('is-dimmed', path.dataset.from !== nodeId && path.dataset.to !== nodeId);
      });
      const rows = connections.map(edge => {
        const outgoing = edge.from === nodeId;
        const localPort = outgoing ? edge.from_port : edge.to_port;
        const remoteDevice = outgoing ? edge.to_device : edge.from_device;
        const remotePort = outgoing ? edge.to_port : edge.from_port;
        return `<li><strong>${escapeHtml(localPort)}</strong><span>→ ${escapeHtml(remoteDevice)} · ${escapeHtml(remotePort)}</span><small>${isWirelessEdge(edge) ? 'WLAN · ' : ''}${escapeHtml(edge.label)}</small></li>`;
      }).join('');
      showTooltip(`<strong>Verbindungen (${connections.length})</strong><ul>${rows || '<li>Keine Verbindungen</li>'}</ul>`, node);
    });
    node.addEventListener('mouseleave', () => {
      svg.querySelectorAll('.rack-topology-edge').forEach(path => path.classList.remove('is-highlighted', 'is-dimmed'));
      hideTooltip();
    });

    let drag = null;
    node.addEventListener('pointerdown', event => {
      if (event.button !== 0) return;
      const saved = storedPositions[nodeId] || {x: 0, y: 0};
      drag = {startX: event.clientX, startY: event.clientY, x: saved.x, y: saved.y, moved: false};
      node.setPointerCapture(event.pointerId);
      node.classList.add('is-dragging');
      event.preventDefault();
    });
    node.addEventListener('pointermove', event => {
      if (!drag) return;
      const x = drag.x + (event.clientX - drag.startX) / topologyZoom;
      const y = drag.y + (event.clientY - drag.startY) / topologyZoom;
      drag.moved ||= Math.abs(x - drag.x) > 3 || Math.abs(y - drag.y) > 3;
      node.style.transform = `translate(${x}px, ${y}px)`;
      storedPositions[nodeId] = {x, y};
      drawEdges();
    });
    node.addEventListener('pointerup', event => {
      if (!drag) return;
      const moved = drag.moved;
      drag = null;
      node.releasePointerCapture(event.pointerId);
      node.classList.remove('is-dragging');
      localStorage.setItem(storageKey, JSON.stringify(storedPositions));
      if (moved) node.dataset.suppressClick = 'true';
    });
    node.addEventListener('click', event => {
      if (node.dataset.suppressClick === 'true') {
        event.preventDefault();
        delete node.dataset.suppressClick;
      }
    });
  });

  resetButton?.addEventListener('click', () => {
    storedPositions = {};
    localStorage.removeItem(storageKey);
    root.querySelectorAll('.rack-topology-node').forEach(node => node.style.transform = '');
    drawEdges();
  });
  fullscreenButton?.addEventListener('click', async () => {
    if (document.fullscreenElement === topologyCard) await document.exitFullscreen();
    else await topologyCard.requestFullscreen();
  });
  document.addEventListener('fullscreenchange', () => {
    const active = document.fullscreenElement === topologyCard;
    fullscreenButton.innerHTML = active
      ? '<i class="mdi mdi-fullscreen-exit"></i> Vollbild verlassen'
      : '<i class="mdi mdi-fullscreen"></i> Vollbild';
    requestAnimationFrame(drawEdges);
  });
  const setTopologyZoom = value => {
    topologyZoom = Math.min(2, Math.max(.5, Math.round(value * 10) / 10));
    columns.style.zoom = topologyZoom;
    zoomLabel.textContent = `${Math.round(topologyZoom * 100)}%`;
    requestAnimationFrame(drawEdges);
  };
  zoomInButton?.addEventListener('click', () => setTopologyZoom(topologyZoom + .1));
  zoomOutButton?.addEventListener('click', () => setTopologyZoom(topologyZoom - .1));
  zoomResetButton?.addEventListener('click', () => setTopologyZoom(1));

  const xmlEscape = value => String(value ?? '').replace(/[&<>"']/g, char => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;'
  })[char]);
  const safeName = value => String(value || 'export').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-|-$/g, '');
  const exportPalette = () => {
    const declaredTheme = activeNetBoxTheme();
    const dark = declaredTheme === 'dark' || (!declaredTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
    return dark ? {
      frame: '#52606d', grid: '#667382', heading: '#8fc8ff', muted: '#a9b7c6',
      deviceFill: '#162e45', deviceText: '#ffffff', deviceSubtext: '#cad5df',
    } : {
      frame: '#718096', grid: '#8b99a8', heading: '#0b69b7', muted: '#52606d',
      deviceFill: '#eef6ff', deviceText: '#132238', deviceSubtext: '#52606d',
    };
  };
  const svgDocument = (width, height, content) =>
    `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${content}</svg>`;
  const drawioDocument = cells =>
    `<mxfile host="app.diagrams.net" type="device"><diagram name="Rack export"><mxGraphModel page="0" background="none" grid="1" guides="1" connect="1"><root><mxCell id="0"/><mxCell id="1" parent="0"/>${cells}</root></mxGraphModel></diagram></mxfile>`;

  const renderedRackData = racks => racks.map(rack => {
    const card = document.querySelector(`.rack-card[data-rack-id="${rack.id}"]`);
    if (!card) return rack;
    const rackUnits = Number.parseFloat(card.dataset.rackUnits) || rack.u_height || 1;
    const devices = Array.from(card.querySelectorAll('.rack-device')).map(device => {
      const bottom = Number.parseFloat(device.style.bottom) || 0;
      const height = Number.parseFloat(device.style.height) || (100 / rackUnits);
      return {
        name: device.dataset.deviceName || device.querySelector('span')?.textContent || 'Gerät',
        type: device.dataset.deviceType || '',
        position: bottom / 100 * rackUnits + 1,
        height: Math.max(height / 100 * rackUnits, 1),
        face: device.dataset.face === 'rear' ? 'rear' : 'front',
        color: device.dataset.deviceColor || '#1685fc',
      };
    });
    return {...rack, devices};
  });

  const buildRackDrawio = racks => {
    const palette = exportPalette();
    const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55;
    const rackWidth = labelWidth + faceWidth * 2 + faceGap;
    let cells = '', cellId = 2;
    const vertex = (value, style, x, y, width, height) => {
      const id = cellId++;
      cells += `<mxCell id="${id}" value="${xmlEscape(value)}" style="${style}" vertex="1" parent="1"><mxGeometry x="${x}" y="${y}" width="${width}" height="${height}" as="geometry"/></mxCell>`;
      return id;
    };
    racks.forEach((rack, rackIndex) => {
      const baseX = 10 + rackIndex * (rackWidth + rackGap);
      const rackHeight = rack.u_height * unit;
      vertex(rack.name, `text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=16;fontStyle=1;fontColor=${palette.heading};`, baseX, 0, rackWidth, 25);
      vertex(`${rack.site}${rack.location ? ` · ${rack.location}` : ''} · ${rack.u_height}U`, `text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=10;fontColor=${palette.muted};`, baseX, 24, rackWidth, 20);
      ['front', 'rear'].forEach((face, faceIndex) => {
        const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap);
        vertex(face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE', `text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=10;fontColor=${palette.muted};`, x, 38, faceWidth, 17);
        vertex('', `rounded=0;html=1;fillColor=none;strokeColor=${palette.frame};strokeWidth=4;`, x, header, faceWidth, rackHeight);
        for (let u = 1; u <= rack.u_height; u++) {
          const y = header + rackHeight - u * unit;
          if (faceIndex === 0) vertex(String(u), `text;html=1;strokeColor=none;fillColor=none;align=right;fontSize=8;fontColor=${palette.muted};`, x - labelWidth, y, labelWidth - 4, unit);
        }
        rack.devices.filter(device => device.face === face).forEach(device => {
          const y = header + rackHeight - (device.position - 1 + device.height) * unit;
          const h = Math.max(device.height * unit, 4);
          const value = h >= 31 ? `${device.name}<br><font style="font-size:9px">U${device.position} · ${device.type}</font>` : device.name;
          vertex(value, `rounded=1;html=1;whiteSpace=wrap;overflow=hidden;align=left;verticalAlign=top;spacingLeft=5;spacingTop=2;fillColor=${palette.deviceFill};strokeColor=${device.color};strokeWidth=2;fontColor=${palette.deviceText};fontSize=11;`, x + 3, y + 1, faceWidth - 6, h - 2);
        });
      });
    });
    return drawioDocument(cells);
  };

  const buildRackSvg = racks => {
    const palette = exportPalette();
    const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55;
    const rackWidth = labelWidth + faceWidth * 2 + faceGap;
    const maxUnits = Math.max(...racks.map(rack => rack.u_height || 1), 1);
    const width = racks.length * rackWidth + Math.max(0, racks.length - 1) * rackGap + 20;
    const height = header + maxUnits * unit + 25;
    let content = '';
    racks.forEach((rack, rackIndex) => {
      const baseX = 10 + rackIndex * (rackWidth + rackGap);
      const rackHeight = rack.u_height * unit;
      content += `<text x="${baseX}" y="20" fill="${palette.heading}" font-family="sans-serif" font-size="16" font-weight="700">${xmlEscape(rack.name)}</text>`;
      content += `<text x="${baseX}" y="39" fill="${palette.muted}" font-family="sans-serif" font-size="11">${xmlEscape(rack.site)}${rack.location ? ` · ${xmlEscape(rack.location)}` : ''} · ${rack.u_height}U</text>`;
      ['front', 'rear'].forEach((face, faceIndex) => {
        const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap);
        content += `<text x="${x + faceWidth / 2}" y="51" text-anchor="middle" fill="${palette.muted}" font-family="sans-serif" font-size="10">${face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE'}</text>`;
        content += `<rect x="${x}" y="${header}" width="${faceWidth}" height="${rackHeight}" fill="none" stroke="${palette.frame}" stroke-width="4"/>`;
        for (let u = 1; u <= rack.u_height; u++) {
          const y = header + rackHeight - u * unit;
          content += `<line x1="${x}" y1="${y}" x2="${x + faceWidth}" y2="${y}" stroke="${palette.grid}" stroke-opacity=".35"/>`;
          if (faceIndex === 0) content += `<text x="${x - 6}" y="${y + 15}" text-anchor="end" fill="${palette.muted}" font-family="sans-serif" font-size="9">${u}</text>`;
        }
        rack.devices.filter(device => device.face === face).forEach(device => {
          const y = header + rackHeight - (device.position - 1 + device.height) * unit;
          const h = Math.max(device.height * unit, 4);
          content += `<rect x="${x + 3}" y="${y + 1}" width="${faceWidth - 6}" height="${h - 2}" rx="3" fill="${palette.deviceFill}" stroke="${xmlEscape(device.color)}" stroke-width="2"/>`;
          if (h >= 13) content += `<text x="${x + 9}" y="${y + Math.min(15, h - 4)}" fill="${palette.deviceText}" font-family="sans-serif" font-size="11" font-weight="600">${xmlEscape(device.name.slice(0, 34))}</text>`;
          if (h >= 31) content += `<text x="${x + 9}" y="${y + 28}" fill="${palette.deviceSubtext}" font-family="sans-serif" font-size="9">U${device.position} · ${xmlEscape(device.type.slice(0, 34))}</text>`;
        });
      });
    });
    return {svg: svgDocument(width, height, content), drawio: buildRackDrawio(racks), width, height};
  };

  const buildTopologySvg = () => {
    const palette = exportPalette();
    const rootBounds = root.getBoundingClientRect();
    let width = Math.max(root.clientWidth, columns.scrollWidth * topologyZoom + 32);
    let height = Math.max(root.clientHeight, columns.scrollHeight * topologyZoom + 32);
    const positions = new Map();
    const nodeCells = new Map();
    let content = '';
    let drawioCells = '', drawioId = 2;
    root.querySelectorAll('.rack-topology-column').forEach(column => {
      const heading = column.querySelector('h4');
      const headingBounds = heading.getBoundingClientRect();
      const headingX = headingBounds.left - rootBounds.left + root.scrollLeft;
      const headingY = headingBounds.top - rootBounds.top + root.scrollTop;
      content += `<text x="${headingX + headingBounds.width / 2}" y="${headingY + 15}" text-anchor="middle" fill="${palette.heading}" font-family="sans-serif" font-size="13" font-weight="600">${xmlEscape(heading.textContent)}</text>`;
      drawioCells += `<mxCell id="group-${drawioId++}" value="${xmlEscape(heading.textContent)}" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontStyle=1;fontColor=${palette.heading};" vertex="1" parent="1"><mxGeometry x="${headingX}" y="${headingY}" width="${headingBounds.width}" height="22" as="geometry"/></mxCell>`;
    });
    root.querySelectorAll('.rack-topology-node').forEach(node => {
      const bounds = node.getBoundingClientRect();
      const x = bounds.left - rootBounds.left + root.scrollLeft;
      const y = bounds.top - rootBounds.top + root.scrollTop;
      width = Math.max(width, x + bounds.width + 20);
      height = Math.max(height, y + bounds.height + 20);
      positions.set(node.dataset.nodeId, {x: x + bounds.width / 2, y: y + bounds.height / 2});
      const color = getComputedStyle(node).getPropertyValue('--node-color').trim() || '#1685fc';
      const cellId = `node-${drawioId++}`;
      nodeCells.set(node.dataset.nodeId, cellId);
      drawioCells += `<mxCell id="${cellId}" value="${xmlEscape(node.querySelector('strong').textContent)}&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;${xmlEscape(node.querySelector('small').textContent)}&lt;/font&gt;" style="rounded=1;html=1;whiteSpace=wrap;align=left;verticalAlign=middle;spacingLeft=8;fillColor=${palette.deviceFill};strokeColor=${xmlEscape(color)};strokeWidth=2;fontColor=${palette.deviceText};fontSize=12;" vertex="1" parent="1"><mxGeometry x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" as="geometry"/></mxCell>`;
      content += `<rect x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" rx="6" fill="${palette.deviceFill}" stroke="${xmlEscape(color)}" stroke-width="2"/>`;
      content += `<text x="${x + 10}" y="${y + 20}" fill="${palette.deviceText}" font-family="sans-serif" font-size="12" font-weight="600">${xmlEscape(node.querySelector('strong').textContent)}</text>`;
      content += `<text x="${x + 10}" y="${y + 37}" fill="${palette.deviceSubtext}" font-family="sans-serif" font-size="9">${xmlEscape(node.querySelector('small').textContent)}</text>`;
    });
    let edgeContent = '';
    data.edges.forEach(edge => {
      const a = positions.get(edge.from), b = positions.get(edge.to);
      if (!a || !b) return;
      const bend = Math.max(30, Math.abs(b.x - a.x) * .35);
      edgeContent += `<path d="M ${a.x} ${a.y} C ${a.x + bend} ${a.y}, ${b.x - bend} ${b.y}, ${b.x} ${b.y}" fill="none" stroke="${xmlEscape(edge.color)}" stroke-width="3"${isWirelessEdge(edge) ? ' stroke-dasharray="7 7"' : ''}/>`;
      const sourceId = nodeCells.get(edge.from), targetId = nodeCells.get(edge.to);
      if (sourceId && targetId) {
        drawioCells += `<mxCell id="edge-${drawioId++}" value="${xmlEscape(isWirelessEdge(edge) ? `WLAN · ${edge.label}` : edge.label)}" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeColor=${xmlEscape(edge.color)};strokeWidth=2;fontSize=9;${isWirelessEdge(edge) ? 'dashed=1;dashPattern=7 7;' : ''}" edge="1" parent="1" source="${sourceId}" target="${targetId}"><mxGeometry relative="1" as="geometry"/></mxCell>`;
      }
    });
    return {svg: svgDocument(width, height, edgeContent + content), drawio: drawioDocument(drawioCells), width, height};
  };

  const downloadBlob = (blob, filename) => {
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = filename;
    link.click();
    setTimeout(() => URL.revokeObjectURL(link.href), 1000);
  };
  const asDrawio = ({svg, drawio, width, height}) => {
    if (drawio) return drawio;
    const image = `data:image/svg+xml,${encodeURIComponent(svg)}`;
    return `<mxfile host="app.diagrams.net" type="device"><diagram name="Rack export"><mxGraphModel page="0" background="none"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="2" value="" style="shape=image;imageAspect=0;aspect=fixed;image=${xmlEscape(image)};" vertex="1" parent="1"><mxGeometry x="0" y="0" width="${width}" height="${height}" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>`;
  };
  const exportGraphic = (graphic, format, filename) => {
    if (format === 'svg') {
      downloadBlob(new Blob([graphic.svg], {type: 'image/svg+xml'}), `${filename}.svg`);
    } else if (format === 'drawio') {
      downloadBlob(new Blob([asDrawio(graphic)], {type: 'application/xml'}), `${filename}.drawio`);
    } else {
      const image = new Image();
      const url = URL.createObjectURL(new Blob([graphic.svg], {type: 'image/svg+xml'}));
      image.onload = () => {
        const scale = 2;
        const canvas = document.createElement('canvas');
        canvas.width = graphic.width * scale;
        canvas.height = graphic.height * scale;
        const context = canvas.getContext('2d');
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.scale(scale, scale);
        context.drawImage(image, 0, 0);
        canvas.toBlob(blob => downloadBlob(blob, `${filename}.png`), 'image/png');
        URL.revokeObjectURL(url);
      };
      image.src = url;
    }
  };

  document.querySelectorAll('.rack-export').forEach(button => button.addEventListener('click', () => {
    const selectedRacks = button.dataset.scope === 'all' ? rackData : rackData.filter(rack => String(rack.id) === button.dataset.scope);
    const racks = renderedRackData(selectedRacks);
    const name = button.dataset.scope === 'all' ? 'racks-gesamt' : `rack-${safeName(racks[0]?.name)}`;
    exportGraphic(buildRackSvg(racks), button.dataset.format, name);
  }));
  document.querySelectorAll('.topology-export').forEach(button => button.addEventListener('click', () => {
    exportGraphic(buildTopologySvg(), button.dataset.format, 'rack-topologie');
  }));
  requestAnimationFrame(drawEdges);
  window.addEventListener('resize', drawEdges);
  root.addEventListener('scroll', drawEdges, {passive: true});
})();
</script>
{% endblock %}
