{% extends "base.html" %}

{% block css %}
{{ super() }}

/* --- Map --- */
#map {
    width: 100%;
    height: 400px;
    margin-bottom: 20px;
    border-radius: 8px;
    display: block;
}
.leaflet-container {
    background: #1a1a1a;
    z-index: 1;
}

/* --- Node Info --- */
.node-info {
    background-color: #1f2226;
    border: 1px solid #3a3f44;
    color: #ddd;
    font-size: 0.88rem;
    padding: 12px 14px;
    margin-bottom: 14px;
    border-radius: 8px;

    display: grid;
    grid-template-columns: repeat(3, minmax(120px, 1fr));
    grid-column-gap: 14px;
    grid-row-gap: 6px;
}

.node-info div { padding: 2px 0; }
.node-info strong {
    color: #9fd4ff;
    font-weight: 600;
}

/* --- Charts --- */
.chart-container {
    width: 100%;
    height: 380px;
    margin-bottom: 25px;
    border: 1px solid #3a3f44;
    border-radius: 8px;
    overflow: hidden;
    background-color: #16191d;
}
.chart-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: #1f2226;
    padding: 6px 12px;
    font-weight: bold;
    border-bottom: 1px solid #333;
    font-size: 1rem;
}
.chart-actions button {
    background: rgba(255,255,255,0.05);
    border: 1px solid #555;
    border-radius: 4px;
    color: #ccc;
    font-size: 0.8rem;
    padding: 2px 6px;
    cursor: pointer;
}
.chart-actions button:hover {
    color: #fff;
    background: rgba(255,255,255,0.15);
    border-color: #888;
}

/* --- Packet Table --- */
.packet-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
    color: #e4e9ee;
}
.packet-table th, .packet-table td {
    border: 1px solid #3a3f44;
    padding: 6px 10px;
    text-align: left;
}
.packet-table th {
    background-color: #1f2226;
    font-weight: bold;
}
.packet-table tr:nth-of-type(odd) { background-color: #272b2f; }
.packet-table tr:nth-of-type(even) { background-color: #212529; }

.port-tag {
    padding: 2px 6px;
    border-radius: 6px;
    font-size: 0.75rem;
    color: #fff;
}

.to-mqtt { font-style: italic; color: #aaa; }

.payload-row { display: none; background-color: #1b1e22; }
.payload-cell {
    padding: 8px 12px;
    font-family: monospace;
    white-space: pre-wrap;
    color: #b0bec5;
}
.packet-table tr.expanded + .payload-row { display: table-row; }
.toggle-btn { cursor: pointer; color: #aaa; margin-right: 6px; }
.toggle-btn:hover { color: #fff; }

/* --- Chart Modal --- */
#chartModal {
    display:none; position:fixed; top:0; left:0; width:100%; height:100%;
    background:rgba(0,0,0,0.9); z-index:9999;
    align-items:center; justify-content:center;
}
#chartModal > div {
    background:#1b1e22; border-radius:8px;
    width:90%; height:85%; padding:10px;
}

/* Inline link */
.inline-link {
    margin-left: 6px;
    font-weight: bold;
    text-decoration: none;
    color: #9fd4ff;
}
.inline-link:hover { color: #c7e6ff; }
{% endblock %}

{% block body %}
<div class="container">

    <h5 class="mb-3">
        📡 <span data-translate-lang="specifications">Specifications:</span><strong>:</strong>
        <span id="nodeLabel"></span>
    </h5>

    <!-- Node Info -->
    <div id="node-info" class="node-info">
        <div><strong data-translate-lang="node_id">Node ID</strong><strong>: </strong><span id="info-node-id">—</span></div>
        <div><strong data-translate-lang="id">Hex ID</strong><strong>: </strong><span id="info-id">—</span></div>
        <div><strong data-translate-lang="long_name">Long Name</strong><strong>: </strong> <span id="info-long-name">—</span></div>
        <div><strong data-translate-lang="short_name">Short Name</strong><strong>: </strong> <span id="info-short-name">—</span></div>

        <div><strong data-translate-lang="hw_model">Hardware Model</strong><strong>: </strong> <span id="info-hw-model">—</span></div>
        <div><strong data-translate-lang="firmware">Firmware</strong><strong>: </strong> <span id="info-firmware">—</span></div>
        <div><strong data-translate-lang="role">Role</strong><strong>: </strong> <span id="info-role">—</span></div>

        <div><strong data-translate-lang="channel">Channel</strong><strong>: </strong> <span id="info-channel">—</span></div>
        <div><strong data-translate-lang="latitude">Latitude</strong><strong>: </strong> <span id="info-lat">—</span></div>
        <div><strong data-translate-lang="longitude">Longitude</strong><strong>: </strong> <span id="info-lon">—</span></div>

        <div><strong data-translate-lang="last_update">Last Update</strong><strong>: </strong> <span id="info-last-update">—</span></div>
        <div>
            <strong data-translate-lang="statistics">Statistics</strong><strong>: </strong>
            <span id="info-stats"
                  data-label-24h="24h"
                  data-label-sent="Packets sent"
                  data-label-seen="Times seen">—</span>
        </div>


    </div>

    <!-- Map. -->
    <div id="map"></div>

    <!-- Battery Chart -->
    <div id="battery_voltage_container" class="chart-container">
        <div class="chart-header">
            🔋 <span data-translate-lang="battery_voltage">Battery & Voltage</span>
            <div class="chart-actions">
                <button onclick="expandChart('battery_voltage')" data-translate-lang="expand">Expand</button>
                <button onclick="exportCSV('battery_voltage')" data-translate-lang="export_csv">Export CSV</button>
            </div>
        </div>
        <div id="chart_battery_voltage" style="height:380px;"></div>
    </div>

    <!-- Air/Channel -->
    <div id="air_channel_container" class="chart-container">
        <div class="chart-header">
            📶 <span data-translate-lang="air_channel">Air & Channel Utilization</span>
            <div class="chart-actions">
                <button onclick="expandChart('air_channel')" data-translate-lang="expand">Expand</button>
                <button onclick="exportCSV('air_channel')" data-translate-lang="export_csv">Export CSV</button>
            </div>
        </div>
        <div id="chart_air_channel" style="height:380px;"></div>
    </div>

    <!-- Env Metrics -->
    <div id="env_chart_container" class="chart-container" style="display:none;">
        <div class="chart-header">
            🌡️ <span data-translate-lang="environment">Environment Metrics</span>
            <div class="chart-actions">
                <button onclick="expandChart('environment')" data-translate-lang="expand">Expand</button>
                <button onclick="exportCSV('environment')" data-translate-lang="export_csv">Export CSV</button>
            </div>
        </div>
        <div id="chart_environment" style="height:380px;"></div>
    </div>

    <!-- Neighbor chart -->
<!-- Neighbor Time-Series Chart -->
<div id="neighbor_chart_container" class="chart-container">
    <div class="chart-header">
        📡 <span data-translate-lang="neighbors_chart">Neighbors (SNR Over Time)</span>
        <div class="chart-actions">
            <button onclick="expandChart('neighbors')" data-translate-lang="expand">Expand</button>
            <button onclick="exportCSV('neighbors')" data-translate-lang="export_csv">Export CSV</button>
        </div>
    </div>
    <div id="chart_neighbors" style="height:380px;"></div>
</div>

    <!-- Packet Histogram -->
<div id="packet_histogram_container" class="chart-container">
    <div class="chart-header">
        📊 <span data-translate-lang="packets_per_day">Packets per Day (Last 7 Days)</span>
        <div class="chart-actions">
            <button onclick="expandChart('packet_histogram')" data-translate-lang="expand">Expand</button>
            <button onclick="exportCSV('packet_histogram')" data-translate-lang="export_csv">Export CSV</button>
        </div>
    </div>
    <div id="chart_packet_histogram" style="height:380px;"></div>
</div>

    <!-- Packet Filters -->
<div class="filter-container" style="margin-bottom:10px; display:flex; gap:12px; flex-wrap:wrap;">
    <select id="packet_since">
        <option value="">All time</option>
        <option value="3600">Last hour</option>
        <option value="21600">Last 6 hours</option>
        <option value="86400">Last 24 hours</option>
        <option value="172800">Last 2 days</option>
        <option value="259200">Last 3 days</option>
        <option value="432000">Last 5 days</option>
        <option value="604800">Last 7 days</option>
    </select>

    <select id="packet_port">
        <option value="">All ports</option>
    </select>

    <button onclick="reloadPackets()">Apply</button>
    <button onclick="exportPacketsCSV()">Export CSV</button>
</div>




    <!-- Packets -->
    <table class="packet-table">
    <thead>
        <tr>
            <th data-translate-lang="time">Time</th>
            <th data-translate-lang="packet_id">Packet ID</th>
            <th data-translate-lang="from">From</th>
            <th data-translate-lang="to">To</th>
            <th data-translate-lang="port">Port</th>
            <th data-translate-lang="size">Size</th>
        </tr>
    </thead>

        <tbody id="packet_list"></tbody>
    </table>

</div>

<!-- Modal -->
<div id="chartModal">
    <div>
        <div style="text-align:right;">
            <button onclick="closeModal()" style="background:none;border:none;color:#ccc;">✖</button>
        </div>
        <div id="modalChart" style="width:100%; height:90%;"></div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script src="/static/portmaps.js"></script>

<script>

    const PORT_COLOR_MAP = window.PORT_COLOR_MAP || {};
    const PORT_LABEL_MAP = window.PORT_LABEL_MAP || {};

/* ======================================================
   NODE PAGE TRANSLATION (isolated from base)
   ====================================================== */

let nodeTranslations = {};

async function loadTranslationsNode() {
    try {
        const cfg = await window._siteConfigPromise;
        const lang = cfg?.site?.language || "en";

        const res = await fetch(`/api/lang?lang=${lang}&section=node`);
        nodeTranslations = await res.json();

        applyTranslationsNode(nodeTranslations);

        // Broadcast label can be set here since translations are now loaded
        nodeMap[4294967295] = nodeTranslations.all_broadcast || "All";
    } catch (err) {
        console.error("Node translation load failed", err);
    }
}

function applyTranslationsNode(dict, root=document) {
    root.querySelectorAll("[data-translate-lang]").forEach(el => {
        const key = el.dataset.translateLang;

        if (dict[key]) {
            if (el.tagName === "INPUT" && el.placeholder !== undefined) {
                el.placeholder = dict[key];
            } else {
                el.textContent = dict[key];
            }
        }
    });
}

/* ======================================================
    POPUP + TIME HELPERS
   ====================================================== */

function makeNodePopup(node) {
    return `
        <div style="font-size:0.9em">
            <a href="/node/${node.node_id}" style="color:inherit; text-decoration:underline;">
                <b>${node.long_name || node.short_name || node.node_id}</b>
            </a>
            ${node.short_name ? ` (${node.short_name})` : ""}<br>

            <b><span data-translate-lang="node_id">
                ${nodeTranslations.node_id || "Node ID"}:
            </span></b> ${node.node_id}<br>

            <b><span data-translate-lang="hw_model">
                ${nodeTranslations.hw_model || "HW Model"}:
            </span></b> ${node.hw_model ?? "—"}<br>

            <b><span data-translate-lang="channel">
                ${nodeTranslations.channel || "Channel"}:
            </span></b> ${node.channel ?? "—"}<br>

            <b><span data-translate-lang="role">
                ${nodeTranslations.role || "Role"}:
            </span></b> ${node.role ?? "—"}<br>

            <b><span data-translate-lang="firmware">
                ${nodeTranslations.firmware || "Firmware"}:
            </span></b> ${node.firmware ?? "—"}<br>

            <b><span data-translate-lang="last_update">
                ${nodeTranslations.last_update || "Last Update"}:
            </span></b> ${formatLastSeen(node.last_seen_us)}
        </div>
    `;
}

function formatLastSeen(us) {
    if (!us) return "—";
    const d = new Date(us / 1000);
    return d.toLocaleString([], {
        month: "2-digit",
        day: "2-digit",
        hour: "2-digit",
        minute: "2-digit"
    });
}

function formatLocalTime(us){
    return new Date(us / 1000).toLocaleString([], {
        month: "2-digit",
        day: "2-digit",
        hour: "2-digit",
        minute: "2-digit"
    });
}

/* ======================================================
    GLOBALS
   ====================================================== */

let nodeMap = {};          // node_id -> label
let nodePositions = {};    // node_id -> [lat, lon]
let nodeCache = {};        // node_id -> full node object
let currentNode = null;
let currentPacketRows = [];

let map, markers = {};
let chartData = {}, neighborData = { ids:[], names:[], snrs:[] };

let fromNodeId = new URLSearchParams(window.location.search).get("from_node_id");
if (!fromNodeId) {
    const parts = window.location.pathname.split("/");
    fromNodeId = parts[parts.length - 1];
}

/* ======================================================
    API HELPERS (USE /api/nodes?node_id=...)
   ====================================================== */

async function fetchNodeFromApi(nodeId) {
    if (nodeCache[nodeId]) return nodeCache[nodeId];

    try {
        const res = await fetch(`/api/nodes?node_id=${encodeURIComponent(nodeId)}`);
        if (!res.ok) {
            console.error("Failed /api/nodes?node_id=", nodeId, res.status);
            return null;
        }
        const data = await res.json();
        const node = (data.nodes || [])[0];
        if (!node) return null;

        nodeCache[nodeId] = node;

        const label = node.long_name || node.short_name || node.id || node.node_id;
        nodeMap[node.node_id] = label;

        if (node.last_lat && node.last_long) {
            nodePositions[node.node_id] = [node.last_lat / 1e7, node.last_long / 1e7];
        }

        return node;
    } catch (err) {
        console.error("Error fetching node", nodeId, err);
        return null;
    }
}

/* ======================================================
    LOAD NODE INFO (SINGLE NODE)
   ====================================================== */

async function loadNodeInfo(){
    try {
        const node = await fetchNodeFromApi(fromNodeId);
        currentNode = node;

        if (!node) {
            document.getElementById("node-info").style.display = "none";
            return;
        }

        // Label in title
        document.getElementById("nodeLabel").textContent =
            nodeMap[fromNodeId] || fromNodeId;

        // Info card
        document.getElementById("info-id").textContent    = node.id ?? "—";
        document.getElementById("info-node-id").textContent    = node.node_id ?? "—";
        document.getElementById("info-long-name").textContent  = node.long_name ?? "—";
        document.getElementById("info-short-name").textContent = node.short_name ?? "—";
        document.getElementById("info-hw-model").textContent   = node.hw_model ?? "—";
        document.getElementById("info-firmware").textContent   = node.firmware ?? "—";
        document.getElementById("info-role").textContent       = node.role ?? "—";
        document.getElementById("info-channel").textContent    = node.channel ?? "—";

        document.getElementById("info-lat").textContent =
            node.last_lat ? (node.last_lat / 1e7).toFixed(6) : "—";
        document.getElementById("info-lon").textContent =
            node.last_long ? (node.last_long / 1e7).toFixed(6) : "—";

        let lastSeen = "—";
        if (node.last_seen_us) {
            lastSeen = formatLastSeen(node.last_seen_us);
        }
        document.getElementById("info-last-update").textContent = lastSeen;
        loadNodeStats(node.node_id);
    } catch (err) {
        console.error("Failed to load node info:", err);
        document.getElementById("node-info").style.display = "none";
    }
}

/* ======================================================
    NODE LINK RENDERING
   ====================================================== */

function nodeLink(id, labelOverride = null) {
    // Broadcast
    if (id === 4294967295) {
        return `<span class="to-mqtt" data-translate-lang="all_broadcast">
                    ${nodeTranslations.all_broadcast || "All"}
                </span>`;
    }

    // Direct to MQTT
    if (id === 1) {
        return `<span class="to-mqtt" data-translate-lang="direct_to_mqtt">
                    ${nodeTranslations.direct_to_mqtt || "Direct to MQTT"}
                </span>`;
    }

    // Normal node
    const label = labelOverride || nodeMap[id] || id;

    return `<a href="/node/${id}" style="text-decoration:underline; color:inherit;">
                ${label}
            </a>`;
}

function initPacketPortFilter() {
    const sel = document.getElementById("packet_port");
    if (!sel) return;

    Object.keys(PORT_LABEL_MAP)
        .map(Number)
        .sort((a, b) => a - b)
        .forEach(p => {
            const opt = document.createElement("option");
            opt.value = p;
            opt.textContent = `${PORT_LABEL_MAP[p]} (${p})`;
            sel.appendChild(opt);
        });
}


/* ======================================================
    PORT LABELS
   ====================================================== */

function portLabel(p) {
    const color = PORT_COLOR_MAP[p] || "#6c757d";
    const label = PORT_LABEL_MAP[p] || `Port ${p}`;

    return `
        <span class="port-tag"
              style="background-color:${color}"
              data-no-translate>
            ${label}
        </span>
        <span class="text-secondary">(${p})</span>
    `;
}


/* ======================================================
    MAP SETUP
   ====================================================== */

function initMap(){
    map = L.map('map', { preferCanvas:true }).setView([37.7749, -122.4194], 8);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution:'&copy; OpenStreetMap'
    }).addTo(map);
}

function hideMap(){
    const mapDiv = document.getElementById("map");
    if (mapDiv) {
        mapDiv.style.display = "none";
    }
}

function addMarker(id, lat, lon, color = "red", node = null) {
    if (!map) return;
    if (isNaN(lat) || isNaN(lon)) return;

    nodePositions[id] = [lat, lon];

    if (!node) {
        node = nodeCache[id] || null;
    }

    const popupHtml = node ? makeNodePopup(node) : `<b>${id}</b>`;

    const m = L.circleMarker([lat, lon], {
        radius: 6,
        color,
        fillColor: color,
        fillOpacity: 1
    }).addTo(map).bindPopup(popupHtml);

    markers[id] = m;
    m.bringToFront();
}

async function drawNeighbors(src, nids) {
    if (!map) return;

    // Ensure source node position exists
    const srcNode = await fetchNodeFromApi(src);
    if (!srcNode || !srcNode.last_lat || !srcNode.last_long) return;

    const srcLat = srcNode.last_lat / 1e7;
    const srcLon = srcNode.last_long / 1e7;
    nodePositions[src] = [srcLat, srcLon];

    for (const nid of nids) {
        const neighbor = await fetchNodeFromApi(nid);
        if (!neighbor || !neighbor.last_lat || !neighbor.last_long) continue;

        const lat = neighbor.last_lat / 1e7;
        const lon = neighbor.last_long / 1e7;

        nodePositions[nid] = [lat, lon];

        // Marker
        addMarker(nid, lat, lon, "blue", neighbor);

        // Link line
        L.polyline(
            [[srcLat, srcLon], [lat, lon]],
            { color: "gray", weight: 1 }
        ).addTo(map);
    }

    ensureMapVisible();
}


function ensureMapVisible(){
    if (!map) return;
    requestAnimationFrame(() => {
        map.invalidateSize();
        const group = L.featureGroup(Object.values(markers));
        if (group.getLayers().length > 0) {
            map.fitBounds(group.getBounds(), {
                padding: [20, 20],
                maxZoom: 11
            });
        }
    });
}

/* ======================================================
    POSITION TRACK (portnum=3)
   ====================================================== */

async function loadTrack(){
    try {
        const url = new URL("/api/packets", window.location.origin);
        url.searchParams.set("portnum", 3);
        url.searchParams.set("from_node_id", fromNodeId);
        url.searchParams.set("limit", 50);

        const res = await fetch(url);
        if (!res.ok) {
            hideMap();
            return;
        }

        const data = await res.json();
        const packets = data.packets || [];
        const points = [];

        for (const pkt of packets) {
            if (!pkt.payload) continue;
            const latMatch = pkt.payload.match(/latitude_i:\s*(-?\d+)/);
            const lonMatch = pkt.payload.match(/longitude_i:\s*(-?\d+)/);
            if (!latMatch || !lonMatch) continue;

            const lat = parseInt(latMatch[1], 10) / 1e7;
            const lon = parseInt(lonMatch[1], 10) / 1e7;
            if (isNaN(lat) || isNaN(lon)) continue;

            points.push({
                lat,
                lon,
                time: pkt.import_time_us
            });
        }

        if (!points.length) {
            hideMap();
            return;
        }

        // Sort chronologically (oldest -> newest)
        points.sort((a, b) => a.time - b.time);

        // Track node's last known position
        const latest = points[points.length - 1];
        nodePositions[fromNodeId] = [latest.lat, latest.lon];

        if (!map) {
            initMap();
        }

        const latlngs = points.map(p => [p.lat, p.lon]);
        const trackLine = L.polyline(latlngs, {
            color: '#052152',
            weight: 2
        }).addTo(map);

        const first = points[0];
        const last  = points[points.length - 1];

        const node = currentNode || nodeCache[fromNodeId] || null;

        const startMarker = L.circleMarker([first.lat, first.lon], {
            radius: 6,
            color: 'green',
            fillColor: 'green',
            fillOpacity: 1
        }).addTo(map).bindPopup(node ? makeNodePopup(node) : "Start");

        const endMarker = L.circleMarker([last.lat, last.lon], {
            radius: 6,
            color: 'red',
            fillColor: 'red',
            fillOpacity: 1
        }).addTo(map).bindPopup(node ? makeNodePopup(node) : "Latest");

        markers["__track_start"] = startMarker;
        markers["__track_end"]   = endMarker;

        map.fitBounds(trackLine.getBounds(), { padding:[20,20] });

    } catch (err) {
        console.error("Failed to load track:", err);
        hideMap();
    }
}

/* ======================================================
   PACKETS TABLE + NEIGHBOR OVERLAY
   ====================================================== */

async function loadPackets(filters = {}) {
    const list = document.getElementById("packet_list");
    list.innerHTML = "";

    const url = new URL("/api/packets", window.location.origin);
    url.searchParams.set("node_id", fromNodeId);
    url.searchParams.set("limit", 1000);

    if (filters.since) {
        url.searchParams.set("since", filters.since);
    }

    if (filters.portnum) {
        url.searchParams.set("portnum", filters.portnum);
    }

    const res = await fetch(url);
    if (!res.ok) return;

    const data = await res.json();
    const packets = data.packets || [];
    currentPacketRows = packets;

    for (const pkt of packets.reverse()) {

        // ================================
        // TABLE ROW
        // ================================
        const safePayload = (pkt.payload || "")
            .replace(/[<>]/g, m => (m === "<" ? "&lt;" : "&gt;"));

        const localTime = formatLocalTime(pkt.import_time_us);
        const fromCell  = nodeLink(pkt.from_node_id, pkt.long_name);
        const toCell    = nodeLink(pkt.to_node_id, pkt.to_long_name);

        let inlineLinks = "";

        if (pkt.portnum === 3 && pkt.payload) {
            const latMatch = pkt.payload.match(/latitude_i:\s*(-?\d+)/);
            const lonMatch = pkt.payload.match(/longitude_i:\s*(-?\d+)/);
            if (latMatch && lonMatch) {
                const lat = parseFloat(latMatch[1]) / 1e7;
                const lon = parseFloat(lonMatch[1]) / 1e7;
                inlineLinks +=
                    ` <a class="inline-link" href="https://www.google.com/maps?q=${lat},${lon}" target="_blank">📍</a>`;
            }
        }

        if (pkt.portnum === 70) {
            let traceId = pkt.id;
            const match = pkt.payload?.match(/ID:\s*(\d+)/i);
            if (match) traceId = match[1];
            inlineLinks +=
                ` <a class="inline-link" href="/graph/traceroute/${traceId}" target="_blank">⮕</a>`;
        }

        const sizeBytes = packetSizeBytes(pkt);

        list.insertAdjacentHTML("afterbegin", `
        <tr class="packet-row">
            <td>${localTime}</td>
            <td><span class="toggle-btn">▶</span>
                <a href="/packet/${pkt.id}" style="text-decoration:underline; color:inherit;">
                    ${pkt.id}
                </a>
            </td>
            <td>${fromCell}</td>
            <td>${toCell}</td>
            <td>${portLabel(pkt.portnum)}${inlineLinks}</td>
            <td>${sizeBytes.toLocaleString()} B</td>
        </tr>
        <tr class="payload-row">
            <td colspan="6" class="payload-cell">${safePayload}</td>
        </tr>`);

    }
}



/* ======================================================
    TELEMETRY CHARTS (portnum=67)
   ====================================================== */

async function loadTelemetryCharts(){
    const url = `/api/packets?portnum=67&from_node_id=${fromNodeId}`;
    const res = await fetch(url);
    if (!res.ok) return;

    const data = await res.json();
    const packets = data.packets || [];
    chartData = {
        times: [],
        battery: [], voltage: [],
        airUtil: [], chanUtil: [],
        temperature: [], humidity: [], pressure: []
    };

    for (const pkt of packets.reverse()) {
        const pl = pkt.payload || "";
        const t  = new Date(pkt.import_time_us / 1000);
        chartData.times.push(
            t.toLocaleString([], { month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit" })
        );

        // Matches your payload exactly (inside device_metrics {})
        chartData.battery.push(
            parseFloat(pl.match(/battery_level:\s*([\d.]+)/)?.[1] || NaN)
        );
        chartData.voltage.push(
            parseFloat(pl.match(/voltage:\s*([\d.]+)/)?.[1] || NaN)
        );
        chartData.airUtil.push(
            parseFloat(pl.match(/air_util_tx:\s*([\d.]+)/)?.[1] || NaN)
        );
        chartData.chanUtil.push(
            parseFloat(pl.match(/channel_utilization:\s*([\d.]+)/)?.[1] || NaN)
        );
        chartData.temperature.push(
            parseFloat(pl.match(/temperature:\s*([\d.]+)/)?.[1] || NaN)
        );
        chartData.humidity.push(
            parseFloat(pl.match(/relative_humidity:\s*([\d.]+)/)?.[1] || NaN)
        );
        chartData.pressure.push(
            parseFloat(pl.match(/barometric_pressure:\s*([\d.]+)/)?.[1] || NaN)
        );
    }

    const hasBattery = chartData.battery.some(v => !isNaN(v));
    const hasVoltage = chartData.voltage.some(v => !isNaN(v));
    const hasAir     = chartData.airUtil.some(v => !isNaN(v));
    const hasChan    = chartData.chanUtil.some(v => !isNaN(v));
    const hasEnv     =
        chartData.temperature.some(v => !isNaN(v)) ||
        chartData.humidity.some(v => !isNaN(v))   ||
        chartData.pressure.some(v => !isNaN(v));

    const batteryContainer = document.getElementById("battery_voltage_container");
    const airContainer     = document.getElementById("air_channel_container");
    const envContainer     = document.getElementById("env_chart_container");

    const makeLine = (name, color, data, yAxisIndex = 0) => ({
        name,
        type: 'line',
        smooth: true,
        connectNulls: true,
        yAxisIndex,
        showSymbol: true,
        symbol: 'circle',
        symbolSize: 8,
        lineStyle: {
            width: 2,
            color,
            shadowColor: color.replace('1)', '0.4)'),
            shadowBlur: 8,
            shadowOffsetY: 3
        },
        itemStyle: {
            color,
            borderColor: '#000',
            borderWidth: 1
        },
        areaStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0,   color: color.replace('1)', '0.65)') },
                { offset: 0.5, color: color.replace('1)', '0.35)') },
                { offset: 1,   color: 'rgba(0,0,0,0)' }
            ])
        },
        data: data.map(v => isNaN(v) ? null : v)
    });

    let chart1 = null, chart2 = null, chart3 = null;

    // Battery / Voltage chart
    if (hasBattery || hasVoltage) {
        batteryContainer.style.display = "block";
        chart1 = echarts.init(document.getElementById('chart_battery_voltage'));
        chart1.setOption({
            tooltip: { trigger:'axis' },
            legend: { data:['Battery Level','Voltage'], textStyle:{ color:'#ccc' } },
            xAxis: { type:'category', data:chartData.times, axisLabel:{ color:'#ccc' } },
            yAxis: [
                { type:'value', name:'Battery (%)', axisLabel:{ color:'#ccc' } },
                { type:'value', name:'Voltage (V)', axisLabel:{ color:'#ccc' } }
            ],
            series: [
                makeLine('Battery Level', 'rgba(255,214,82,1)', chartData.battery),
                makeLine('Voltage', 'rgba(79,155,255,1)', chartData.voltage, 1)
            ]
        });
    } else {
        batteryContainer.style.display = "none";
    }

    // Air / Channel chart
    if (hasAir || hasChan) {
        airContainer.style.display = "block";
        chart2 = echarts.init(document.getElementById('chart_air_channel'));
        chart2.setOption({
            tooltip: { trigger:'axis' },
            legend: { data:['Air Util Tx','Channel Utilization'], textStyle:{ color:'#ccc' } },
            xAxis: { type:'category', data:chartData.times, axisLabel:{ color:'#ccc' } },
            yAxis: { type:'value', name:'%', axisLabel:{ color:'#ccc' } },
            series: [
                makeLine('Air Util Tx', 'rgba(138,255,108,1)', chartData.airUtil),
                makeLine('Channel Utilization', 'rgba(255,102,204,1)', chartData.chanUtil)
            ]
        });
    } else {
        airContainer.style.display = "none";
    }

    // Environment chart
    if (hasEnv) {
        envContainer.style.display = "block";
        chart3 = echarts.init(document.getElementById('chart_environment'));
        chart3.setOption({
            tooltip: { trigger:'axis' },
            legend: { data:['Temperature (°C)','Humidity (%)','Pressure (hPa)'], textStyle:{ color:'#ccc' } },
            xAxis: { type:'category', data:chartData.times, axisLabel:{ color:'#ccc' } },
            yAxis: [
                { type:'value', name:'°C / %', axisLabel:{ color:'#ccc' } },
                { type:'value', name:'hPa', axisLabel:{ color:'#ccc' } }
            ],
            series: [
                makeLine('Temperature (°C)', 'rgba(255,138,82,1)', chartData.temperature),
                makeLine('Humidity (%)', 'rgba(138,255,108,1)', chartData.humidity),
                makeLine('Pressure (hPa)', 'rgba(79,155,255,1)', chartData.pressure, 1)
            ]
        });
    } else {
        envContainer.style.display = "none";
    }

    // Resize charts that exist
    window.addEventListener("resize", () => {
        [chart1, chart2, chart3].forEach(c => { if (c) c.resize(); });
    });
}


async function loadLatestNeighborIds() {
    const url = new URL("/api/packets", window.location.origin);
    url.searchParams.set("from_node_id", fromNodeId);
    url.searchParams.set("portnum", 71);
    url.searchParams.set("limit", 1);   // ✅ ONLY the latest packet

    const res = await fetch(url);
    if (!res.ok) return [];

    const data = await res.json();
    const pkt = data.packets?.[0];
    if (!pkt || !pkt.payload) return [];

    const ids = [];
    const re = /neighbors\s*\{([^}]+)\}/g;
    let m;

    while ((m = re.exec(pkt.payload)) !== null) {
        const id = m[1].match(/node_id:\s*(\d+)/);
        if (id) ids.push(parseInt(id[1], 10));
    }

    return ids;
}

/* ======================================================
    NEIGHBOR CHART (portnum=71)
   ====================================================== */

async function loadNeighborTimeSeries() {
    const container = document.getElementById("neighbor_chart_container");
    const chartEl   = document.getElementById("chart_neighbors");

    const url = `/api/packets?portnum=71&from_node_id=${fromNodeId}&limit=500`;
    const res = await fetch(url);

    if (!res.ok) {
        container.style.display = "none";
        return;
    }

    const data = await res.json();
    const packets = data.packets || [];

    if (!packets.length) {
        container.style.display = "none";
        return;
    }

    // Sort packets chronologically (microseconds)
    packets.sort((a, b) => (a.import_time_us || 0) - (b.import_time_us || 0));

    const neighborHistory = {}; // node_id -> { name, times[], snr[] }

    for (const pkt of packets) {
        if (!pkt.import_time_us || !pkt.payload) continue;

        const ts = pkt.import_time_us; // KEEP NUMERIC TIMESTAMP

        const blockRe = /neighbors\s*\{([^}]+)\}/g;
        let m;

        while ((m = blockRe.exec(pkt.payload)) !== null) {
            const block = m[1];

            const idMatch  = block.match(/node_id:\s*(\d+)/);
            const snrMatch = block.match(/snr:\s*(-?\d+(?:\.\d+)?)/);

            if (!idMatch || !snrMatch) continue;

            const nid = parseInt(idMatch[1], 10);
            const snr = parseFloat(snrMatch[1]);

            // Fetch neighbor metadata once
            const neighbor = await fetchNodeFromApi(nid);

            if (!neighborHistory[nid]) {
                neighborHistory[nid] = {
                    name: neighbor?.short_name ||
                          neighbor?.long_name ||
                          `Node ${nid}`,
                    times: [],
                    snr: []
                };
            }

            neighborHistory[nid].times.push(ts);
            neighborHistory[nid].snr.push(snr);
        }
    }

    // Collect ALL timestamps across neighbors
    const allTimes = new Set();
    Object.values(neighborHistory).forEach(entry => {
        entry.times.forEach(t => allTimes.add(t));
    });

    // Sort timestamps numerically
    const xTimes = Array.from(allTimes).sort((a, b) => a - b);

    const legend = [];
    const series = [];

    for (const entry of Object.values(neighborHistory)) {
        legend.push(entry.name);

        series.push({
            name: entry.name,
            type: "line",
            smooth: true,
            connectNulls: true,
            showSymbol: false,
            data: xTimes.map(t => {
                const idx = entry.times.indexOf(t);
                return idx >= 0 ? entry.snr[idx] : null;
            })
        });
    }

    const chart = echarts.init(chartEl);

    chart.setOption({
        tooltip: {
            trigger: "axis",
            axisPointer: { type: "line" }
        },
        legend: {
            data: legend,
            textStyle: { color: "#ccc" }
        },
        xAxis: {
            type: "category",
            data: xTimes,
            axisLabel: {
                color: "#ccc",
                formatter: value =>
                    new Date(value / 1000).toLocaleString([], {
                        year: "2-digit",
                        month: "2-digit",
                        day: "2-digit",
                        hour: "2-digit",
                        minute: "2-digit"
                    })
            }
        },
        yAxis: {
            type: "value",
            name: "SNR",
            axisLabel: { color: "#ccc" }
        },
        series
    });

    window.addEventListener("resize", () => chart.resize());
}



async function loadPacketHistogram() {
    const DAYS = 7;
    const now = new Date();

    const dayKeys = [];
    const dayLabels = [];

    for (let i = DAYS - 1; i >= 0; i--) {
        const d = new Date(now);
        d.setDate(d.getDate() - i);
        dayKeys.push(d.toISOString().slice(0, 10));
        dayLabels.push(
            d.toLocaleDateString([], { month: "short", day: "numeric" })
        );
    }

    const url = new URL("/api/packets", window.location.origin);
    url.searchParams.set("node_id", fromNodeId);

    // last 7 days only (microseconds)
    const sinceUs = Date.now() * 1000 - (7 * 24 * 60 * 60 * 1_000_000);
    url.searchParams.set("since", sinceUs);

    // modest safety limit (still applies after server-side filter)
    url.searchParams.set("limit", 2000);


    const res = await fetch(url);
    if (!res.ok) return;

    const packets = (await res.json()).packets || [];

    const counts = {};   // { port: { day: count } }
    const ports = new Set();

    for (const pkt of packets) {
        if (!pkt.import_time_us) continue;

        const day = new Date(pkt.import_time_us / 1000)
            .toISOString()
            .slice(0, 10);

        if (!dayKeys.includes(day)) continue;

        const port = pkt.portnum ?? 0;
        ports.add(port);

        counts[port] ??= {};
        counts[port][day] = (counts[port][day] || 0) + 1;
    }

    if (!ports.size) {
        document.getElementById("packet_histogram_container").style.display = "none";
        return;
    }

    const series = Array.from(ports)
        .sort((a, b) => a - b)
        .map(port => ({
            name: PORT_LABEL_MAP[port] || `Port ${port}`,
            type: "bar",
            stack: "total",
            barMaxWidth: 42,
            itemStyle: {
                color: PORT_COLOR_MAP[port] || "#888"
            },
            data: dayKeys.map(d => counts[port]?.[d] || 0)
        }));

    const chart = echarts.init(
        document.getElementById("chart_packet_histogram")
    );

    chart.setOption({
        animation: false,
        tooltip: { trigger: "axis" },
        legend: { textStyle: { color: "#ccc" } },
        xAxis: {
            type: "category",
            data: dayLabels,
            axisLabel: { color: "#ccc" }
        },
        yAxis: {
            type: "value",
            axisLabel: { color: "#ccc" }
        },
        series
    });

    window.addEventListener("resize", () => chart.resize());
}



/* ======================================================
   EXPAND / EXPORT BUTTONS
   ====================================================== */

function expandChart(type){
    const srcEl = document.getElementById(`chart_${type}`);
    if (!srcEl) return;
    const sourceChart = echarts.getInstanceByDom(srcEl);
    if (!sourceChart) return;

    const modal = document.getElementById('chartModal');
    const modalChart = echarts.init(document.getElementById('modalChart'));
    modal.style.display = "flex";
    modalChart.setOption(sourceChart.getOption());
    modalChart.resize();
}
function closeModal(){
    document.getElementById('chartModal').style.display = "none";
}

function exportCSV(type){
    const rows = [["Time"]];

    if (type === "battery_voltage") {
        rows[0].push("Battery Level", "Voltage");
        for (let i = 0; i < chartData.times.length; i++)
            rows.push([chartData.times[i], chartData.battery[i], chartData.voltage[i]]);
    }
    else if (type === "air_channel") {
        rows[0].push("Air Util Tx", "Channel Utilization");
        for (let i = 0; i < chartData.times.length; i++)
            rows.push([chartData.times[i], chartData.airUtil[i], chartData.chanUtil[i]]);
    }
    else if (type === "environment") {
        rows[0].push("Temperature", "Humidity", "Pressure");
        for (let i = 0; i < chartData.times.length; i++)
            rows.push([
                chartData.times[i],
                chartData.temperature[i],
                chartData.humidity[i],
                chartData.pressure[i]
            ]);
    }
    else if (type === "neighbors") {
        rows[0] = ["Neighbor Node ID", "Neighbor Name", "SNR (dB)"];
        for (let i = 0; i < neighborData.ids.length; i++) {
            rows.push([
                neighborData.ids[i],
                neighborData.names[i],
                neighborData.snrs[i]
            ]);
        }
    }

    const csv  = rows.map(r => r.join(",")).join("\n");
    const blob = new Blob([csv], { type:"text/csv" });
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob);
    link.download = `${type}_${fromNodeId}.csv`;
    link.click();
}

/* ======================================================
   EXPAND PAYLOAD ROWS
   ====================================================== */

document.addEventListener("click", e => {
    const btn = e.target.closest(".toggle-btn");
    if (!btn) return;
    const row = btn.closest(".packet-row");
    row.classList.toggle("expanded");
    btn.textContent = row.classList.contains("expanded") ? "▼" : "▶";
});

/* ======================================================
    INIT
   ====================================================== */

document.addEventListener("DOMContentLoaded", async () => {
    await loadTranslationsNode();

    requestAnimationFrame(async () => {
        await loadNodeInfo();

        // ✅ MAP MUST EXIST FIRST
        if (!map) initMap();

        // ✅ DRAW LATEST NEIGHBORS ONCE
        const neighborIds = await loadLatestNeighborIds();
        if (neighborIds.length) {
            await drawNeighbors(fromNodeId, neighborIds);
        }

        // ⚠️ Track may add to map, but must not hide it
        await loadTrack();

        await loadPackets();
        initPacketPortFilter();
        await loadTelemetryCharts();
        await loadNeighborTimeSeries();
        await loadPacketHistogram();

        ensureMapVisible();
        setTimeout(ensureMapVisible, 1000);
        window.addEventListener("resize", ensureMapVisible);
        window.addEventListener("focus", ensureMapVisible);
    });
});



function packetSizeBytes(pkt) {
    if (!pkt) return 0;

    // Prefer raw payload length
    if (pkt.payload) {
        return new TextEncoder().encode(pkt.payload).length;
    }

    // Fallbacks (if you later add protobuf/base64)
    if (pkt.raw_payload) {
        return atob(pkt.raw_payload).length;
    }

    return 0;
}


async function loadNodeStats(nodeId) {
    try {
        const res = await fetch(
            `/api/stats/count?from_node=${nodeId}&period_type=day&length=1`
        );

        if (!res.ok) {
            throw new Error(`HTTP ${res.status}`);
        }

        const data = await res.json();

        const packets = data?.total_packets ?? 0;
        const seen    = data?.total_seen ?? 0;

        document.getElementById("info-stats").textContent =
            `24h · Packets sent: ${packets.toLocaleString()} · Times seen: ${seen.toLocaleString()} `;
    } catch (err) {
        console.error("Failed to load node stats:", err);
        document.getElementById("info-stats").textContent = "—";
    }
}

    function reloadPackets() {
    const sinceSel = document.getElementById("packet_since").value;
    const portSel  = document.getElementById("packet_port").value;

    const filters = {};

    if (sinceSel) {
        const sinceUs = Date.now() * 1000 - (parseInt(sinceSel, 10) * 1_000_000);
        filters.since = sinceUs;
    }

    if (portSel) {
        filters.portnum = portSel;
    }

    loadPackets(filters);
}

    function exportPacketsCSV() {
    if (!currentPacketRows.length) {
        alert("No packets to export.");
        return;
    }

    const rows = [
        ["Time", "Packet ID", "From Node", "To Node", "Port", "Port Name", "Payload"]
    ];

    for (const pkt of currentPacketRows) {
        const time = pkt.import_time_us
            ? new Date(pkt.import_time_us / 1000).toISOString()
            : "";

        const portName = PORT_LABEL_MAP[pkt.portnum] || `Port ${pkt.portnum}`;

        // Escape quotes + line breaks for CSV safety
        const payload = (pkt.payload || "")
            .replace(/"/g, '""')
            .replace(/\r?\n/g, " ");

        rows.push([
            time,
            pkt.id,
            pkt.from_node_id,
            pkt.to_node_id,
            pkt.portnum,
            portName,
            `"${payload}"`
        ]);
    }

    const csv = rows.map(r => r.join(",")).join("\n");
    const blob = new Blob([csv], { type: "text/csv" });

    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob);
    link.download = `packets_${fromNodeId}_${Date.now()}.csv`;
    link.click();
}



</script>
{% endblock %}
