// Tauri v2 with withGlobalTauri: true injects window.__TAURI__
const { invoke } = window.__TAURI__.core;

// --- DOM refs ---
const $ = (sel) => document.querySelector(sel);
const setupOverlay = $('#setup-overlay');
const setupName = $('#setup-name');
const setupBtn = $('#setup-btn');
const nodeIdEl = $('#node-id');
const copyBtn = $('#copy-connect');
const postContent = $('#post-content');
const postBtn = $('#post-btn');
const charCount = $('#char-count');
const feedList = $('#feed-list');
const myPostsList = $('#my-posts-list');
const conversationsList = $('#conversations-list');
const messageRequestsList = $('#message-requests-list');
const messageRequestsSection = $('#message-requests-section');
const connectInput = $('#connect-input');
const connectBtn = $('#connect-btn');
const connectStatus = $('#connect-status');
let peersList = null; // created dynamically inside diagnostics popover
const followsList = $('#follows-list');
// suggestedList removed — Suggested Peers section removed from UI
const syncBtn = $('#sync-btn');
const exportKeyBtn = $('#export-key-btn');
const visibilitySelect = $('#visibility-select');
const circleSelect = $('#circle-select');
const circleNameInput = $('#circle-name-input');
const createCircleBtn = $('#create-circle-btn');
const circlesList = $('#circles-list');
const statPosts = $('#stat-posts');
const statPeers = $('#stat-peers');
const statFollows = $('#stat-follows');
const toastEl = $('#toast');
const profileNameInput = $('#profile-name');
const profileBioInput = $('#profile-bio');
const saveProfileBtn = $('#save-profile-btn');
const redundancyPanel = $('#redundancy-panel');
const dmRecipientSelect = $('#dm-recipient-select');
const dmContent = $('#dm-content');
const dmSendBtn = $('#dm-send-btn');
const anchorsList = $('#anchors-list');
const anchorAddSelect = $('#anchor-add-select');
const anchorAddBtn = $('#anchor-add-btn');
const attachBtn = $('#attach-btn');
const fileInput = $('#file-input');
const attachmentPreview = $('#attachment-preview');
const audiencePendingList = $('#audience-pending-list');
const audienceApprovedList = $('#audience-approved-list');
let connectionsList = null; // created dynamically inside diagnostics popover
let networkSummaryEl = null; // created dynamically inside diagnostics popover
const resetDataBtn = $('#reset-data-btn');

// --- State ---
let currentTab = 'feed';
let connectString = '';
let myNodeId = '';
const POST_MAX_CHARS = 500;
let selectedFiles = []; // { data: ArrayBuffer, mime: string, name: string }
let diagnosticsInterval = null;
let lastDiagUpdate = null;
// Cache fingerprints to avoid destructive reloads when data hasn't changed
let _feedFingerprint = '';
let _myPostsFingerprint = '';
let _messagesFingerprint = '';
let _lastMsgTimestamp = 0; // Track newest message timestamp for tiered polling
let _peopleFingerprint = '';

// --- Identicon generator ---
// Generates a deterministic 5x5 mirrored SVG identicon from a hex node_id.
function generateIdenticon(hexId, size = 20) {
    const bytes = [];
    const hex = hexId.replace(/[^0-9a-fA-F]/g, '');
    for (let i = 0; i < 16 && i * 2 + 1 < hex.length; i++) {
        bytes.push(parseInt(hex.substring(i * 2, i * 2 + 2), 16));
    }
    while (bytes.length < 16) bytes.push(0);

    const hue = ((bytes[0] << 8) | bytes[1]) % 360;
    const sat = 55 + (bytes[2] % 25);
    const lit = 55 + (bytes[3] % 20);
    const fg = `hsl(${hue}, ${sat}%, ${lit}%)`;
    const bg = `hsl(${hue}, ${sat}%, ${lit * 0.25}%)`;

    const cells = [];
    for (let row = 0; row < 5; row++) {
        for (let col = 0; col < 3; col++) {
            const byteIdx = 4 + row * 3 + col;
            const on = byteIdx < bytes.length ? (bytes[byteIdx] & 1) === 1 : false;
            if (on) {
                const cellSize = size / 5;
                cells.push(`<rect x="${col * cellSize}" y="${row * cellSize}" width="${cellSize}" height="${cellSize}" fill="${fg}"/>`);
                const mirrorCol = 4 - col;
                if (mirrorCol !== col) {
                    cells.push(`<rect x="${mirrorCol * cellSize}" y="${row * cellSize}" width="${cellSize}" height="${cellSize}" fill="${fg}"/>`);
                }
            }
        }
    }

    return `<svg class="identicon" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" fill="${bg}" rx="2"/>${cells.join('')}</svg>`;
}

// --- QR Code SVG generator ---
// Minimal QR code encoder for alphanumeric data (connect strings).
// Supports versions 1-6, error correction level L.
function generateQRCodeSVG(text, pixelSize) {
    // GF(256) arithmetic for Reed-Solomon
    const GF_EXP = new Uint8Array(512);
    const GF_LOG = new Uint8Array(256);
    {
        let v = 1;
        for (let i = 0; i < 255; i++) {
            GF_EXP[i] = v;
            GF_LOG[v] = i;
            v <<= 1;
            if (v >= 256) v ^= 0x11d;
        }
        for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255];
    }
    function gfMul(a, b) { return a === 0 || b === 0 ? 0 : GF_EXP[GF_LOG[a] + GF_LOG[b]]; }

    function rsGenPoly(nsym) {
        let g = [1];
        for (let i = 0; i < nsym; i++) {
            const ng = new Array(g.length + 1).fill(0);
            for (let j = 0; j < g.length; j++) {
                ng[j] ^= g[j];
                ng[j + 1] ^= gfMul(g[j], GF_EXP[i]);
            }
            g = ng;
        }
        return g;
    }

    function rsEncode(data, nsym) {
        const gen = rsGenPoly(nsym);
        const out = new Uint8Array(data.length + nsym);
        out.set(data);
        for (let i = 0; i < data.length; i++) {
            const coef = out[i];
            if (coef !== 0) {
                for (let j = 0; j < gen.length; j++) {
                    out[i + j] ^= gfMul(gen[j], coef);
                }
            }
        }
        return out.slice(data.length);
    }

    // Alphanumeric encoding
    const ALNUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
    function encodeAlphanumeric(str) {
        const bits = [];
        function pushBits(val, len) { for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1); }
        const upper = str.toUpperCase();
        // Determine version: data capacity for alphanumeric, EC level L
        // version: [totalCodewords, ecCodewords, dataCapacityAlphanumeric]
        const versions = [
            null,
            [26, 7, 25],    // v1
            [44, 10, 47],   // v2
            [70, 15, 77],   // v3
            [100, 20, 114], // v4
            [134, 26, 154], // v5
            [172, 36, 195], // v6
        ];
        let ver = 0;
        for (let v = 1; v <= 6; v++) {
            if (upper.length <= versions[v][2]) { ver = v; break; }
        }
        if (ver === 0) throw new Error('Text too long for QR v1-6');

        const charCountBits = ver <= 9 ? 9 : 13;
        pushBits(0b0010, 4); // mode: alphanumeric
        pushBits(upper.length, charCountBits);

        for (let i = 0; i < upper.length; i += 2) {
            const a = ALNUM.indexOf(upper[i]);
            if (i + 1 < upper.length) {
                const b = ALNUM.indexOf(upper[i + 1]);
                pushBits(a * 45 + b, 11);
            } else {
                pushBits(a, 6);
            }
        }

        const [totalCW, ecCW] = versions[ver];
        const dataCW = totalCW - ecCW;
        const totalDataBits = dataCW * 8;

        // Terminator
        const remaining = totalDataBits - bits.length;
        const termLen = Math.min(4, remaining);
        for (let i = 0; i < termLen; i++) bits.push(0);
        // Byte-align
        while (bits.length % 8 !== 0) bits.push(0);
        // Pad bytes
        const pads = [0xEC, 0x11];
        let pi = 0;
        while (bits.length < totalDataBits) {
            for (let b = 7; b >= 0; b--) bits.push((pads[pi] >> b) & 1);
            pi = (pi + 1) % 2;
        }

        // Convert to bytes
        const dataBytes = new Uint8Array(dataCW);
        for (let i = 0; i < dataCW; i++) {
            let v2 = 0;
            for (let b = 0; b < 8; b++) v2 = (v2 << 1) | (bits[i * 8 + b] || 0);
            dataBytes[i] = v2;
        }

        const ecBytes = rsEncode(dataBytes, ecCW);
        return { ver, dataBytes, ecBytes, totalCW, ecCW };
    }

    const { ver, dataBytes, ecBytes } = encodeAlphanumeric(text);
    const size = 17 + ver * 4;
    const grid = Array.from({ length: size }, () => new Uint8Array(size));
    const reserved = Array.from({ length: size }, () => new Uint8Array(size));

    function setModule(r, c, val) {
        if (r >= 0 && r < size && c >= 0 && c < size) {
            grid[r][c] = val ? 1 : 0;
            reserved[r][c] = 1;
        }
    }

    // Finder patterns
    function drawFinder(row, col) {
        for (let dr = -1; dr <= 7; dr++) {
            for (let dc = -1; dc <= 7; dc++) {
                const r = row + dr, c = col + dc;
                if (r < 0 || r >= size || c < 0 || c >= size) continue;
                const inOuter = dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6;
                const inInner = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4;
                const onBorder = dr === 0 || dr === 6 || dc === 0 || dc === 6;
                const val = inInner || (inOuter && onBorder);
                setModule(r, c, val);
            }
        }
    }
    drawFinder(0, 0);
    drawFinder(0, size - 7);
    drawFinder(size - 7, 0);

    // Timing patterns
    for (let i = 8; i < size - 8; i++) {
        setModule(6, i, i % 2 === 0);
        setModule(i, 6, i % 2 === 0);
    }

    // Dark module
    setModule(size - 8, 8, 1);

    // Reserve format info areas
    for (let i = 0; i < 8; i++) {
        if (!reserved[8][i]) { reserved[8][i] = 1; }
        if (!reserved[i][8]) { reserved[i][8] = 1; }
        if (!reserved[8][size - 1 - i]) { reserved[8][size - 1 - i] = 1; }
        if (!reserved[size - 1 - i][8]) { reserved[size - 1 - i][8] = 1; }
    }
    reserved[8][8] = 1;

    // Alignment pattern for v2+
    if (ver >= 2) {
        const alignPos = [null, null, [6, 18], [6, 22], [6, 26], [6, 30], [6, 34]][ver];
        for (const ar of alignPos) {
            for (const ac of alignPos) {
                if (reserved[ar][ac]) continue;
                for (let dr = -2; dr <= 2; dr++) {
                    for (let dc = -2; dc <= 2; dc++) {
                        const val = Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0);
                        setModule(ar + dr, ac + dc, val);
                    }
                }
            }
        }
    }

    // Place data bits
    const allBytes = new Uint8Array(dataBytes.length + ecBytes.length);
    allBytes.set(dataBytes);
    allBytes.set(ecBytes, dataBytes.length);

    const dataBits = [];
    for (const b of allBytes) {
        for (let i = 7; i >= 0; i--) dataBits.push((b >> i) & 1);
    }

    let bitIdx = 0;
    let upward = true;
    for (let col = size - 1; col >= 0; col -= 2) {
        if (col === 6) col = 5; // skip timing column
        const rows = upward ? Array.from({ length: size }, (_, i) => size - 1 - i) : Array.from({ length: size }, (_, i) => i);
        for (const row of rows) {
            for (const dc of [0, -1]) {
                const c = col + dc;
                if (c < 0 || c >= size) continue;
                if (reserved[row][c]) continue;
                grid[row][c] = bitIdx < dataBits.length ? dataBits[bitIdx] : 0;
                bitIdx++;
            }
        }
        upward = !upward;
    }

    // Apply mask 0 (checkerboard: (row + col) % 2 === 0)
    for (let r = 0; r < size; r++) {
        for (let c = 0; c < size; c++) {
            if (!reserved[r][c] && (r + c) % 2 === 0) {
                grid[r][c] ^= 1;
            }
        }
    }

    // Format info for EC level L (01), mask 0 (000) → format bits = 0b01000
    // Pre-computed with BCH: 0x77c0 → bits: 111011111000010
    const formatBits = [1,1,1,0,1,1,1,1,1,0,0,0,0,1,0];
    // Place format info
    const formatPositions1 = [[0,8],[1,8],[2,8],[3,8],[4,8],[5,8],[7,8],[8,8],[8,7],[8,5],[8,4],[8,3],[8,2],[8,1],[8,0]];
    const formatPositions2 = [[8,size-1],[8,size-2],[8,size-3],[8,size-4],[8,size-5],[8,size-6],[8,size-7],[size-7,8],[size-6,8],[size-5,8],[size-4,8],[size-3,8],[size-2,8],[size-1,8]];

    for (let i = 0; i < 15; i++) {
        const bit = formatBits[i];
        const [r1, c1] = formatPositions1[i];
        grid[r1][c1] = bit;
        if (i < formatPositions2.length) {
            const [r2, c2] = formatPositions2[i];
            grid[r2][c2] = bit;
        }
    }

    // Render SVG
    const moduleSize = Math.max(1, Math.floor(pixelSize / (size + 8))); // +8 for quiet zone
    const totalSize = (size + 8) * moduleSize;
    let rects = '';
    for (let r = 0; r < size; r++) {
        for (let c = 0; c < size; c++) {
            if (grid[r][c]) {
                rects += `<rect x="${(c + 4) * moduleSize}" y="${(r + 4) * moduleSize}" width="${moduleSize}" height="${moduleSize}"/>`;
            }
        }
    }
    return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalSize}" height="${totalSize}" viewBox="0 0 ${totalSize} ${totalSize}">
        <rect width="${totalSize}" height="${totalSize}" fill="#fff" rx="4"/>
        <g fill="#000">${rects}</g>
    </svg>`;
}

// --- Helpers ---
function toast(msg) {
    toastEl.textContent = msg;
    toastEl.classList.remove('hidden');
    setTimeout(() => toastEl.classList.add('hidden'), 3000);
}

// --- Notifications ---
let _notifiedMessages = new Set();
async function maybeNotify(title, body, tag) {
    if (!('Notification' in window)) return;
    if (Notification.permission === 'default') {
        await Notification.requestPermission();
    }
    if (Notification.permission === 'granted') {
        new Notification(title, { body, tag, silent: false });
    }
}

// --- Popover helpers ---
let popoverOnClose = null;
function openPopover(title, html, opts = {}) {
    const overlay = $('#popover-overlay');
    $('#popover-title').textContent = title;
    $('#popover-body').innerHTML = html;
    overlay.classList.remove('hidden');
    if (opts.onOpen) opts.onOpen();
    popoverOnClose = opts.onClose || null;
}
function closePopover() {
    const overlay = $('#popover-overlay');
    overlay.classList.add('hidden');
    $('#popover-body').innerHTML = '';
    if (diagnosticsInterval) { clearInterval(diagnosticsInterval); diagnosticsInterval = null; }
    if (activityInterval) { clearInterval(activityInterval); activityInterval = null; }
    // Clear dynamic refs from popover content
    networkSummaryEl = null; connectionsList = null; peersList = null;
    if (popoverOnClose) { popoverOnClose(); popoverOnClose = null; }
}
$('#popover-close-btn').addEventListener('click', closePopover);
$('#popover-overlay').addEventListener('click', (e) => {
    if (e.target === $('#popover-overlay')) closePopover();
});

function relativeTime(timestampMs) {
    const now = Date.now();
    const diff = now - timestampMs;
    const secs = Math.floor(diff / 1000);
    if (secs < 60) return 'just now';
    const mins = Math.floor(secs / 60);
    if (mins < 60) return `${mins}m ago`;
    const hours = Math.floor(mins / 60);
    if (hours < 24) return `${hours}h ago`;
    const days = Math.floor(hours / 24);
    if (days === 1) return 'yesterday';
    if (days < 7) return `${days}d ago`;
    return new Date(timestampMs).toLocaleDateString();
}

function peerLabel(nodeId, displayName) {
    if (displayName) return displayName;
    return nodeId.substring(0, 12) + '...';
}

function renderPost(post, index) {
    const authorShort = post.author.substring(0, 12);
    const authorName = post.authorName || authorShort;
    const authorClass = post.isMe ? 'author-me' : '';
    const meTag = post.isMe ? ' (you)' : '';
    const timeStr = relativeTime(post.timestampMs);
    const icon = generateIdenticon(post.author, 22);
    const delay = Math.min(index * 0.04, 0.6);

    let visBadge = '';
    if (post.visibility === 'encrypted-for-me') {
        visBadge = '<span class="vis-badge vis-encrypted-mine">encrypted</span>';
    } else if (post.visibility === 'encrypted') {
        visBadge = '<span class="vis-badge vis-encrypted">encrypted</span>';
    }

    let displayContent;
    if (post.visibility === 'encrypted' && !post.decryptedContent) {
        displayContent = '<span class="encrypted-placeholder">(encrypted)</span>';
    } else if (post.decryptedContent) {
        displayContent = escapeHtml(post.decryptedContent);
    } else {
        displayContent = escapeHtml(post.content);
    }

    const deleteBtn = post.isMe
        ? `<button class="delete-post-btn" data-post-id="${post.id}" title="Delete post">x</button>`
        : '';

    let attachmentsHtml = '';
    if (post.attachments && post.attachments.length > 0) {
        const imgs = post.attachments.filter(a => a.mimeType.startsWith('image/'));
        const vids = post.attachments.filter(a => a.mimeType.startsWith('video/'));
        const auds = post.attachments.filter(a => a.mimeType.startsWith('audio/'));
        const others = post.attachments.filter(a => !a.mimeType.startsWith('image/') && !a.mimeType.startsWith('video/') && !a.mimeType.startsWith('audio/'));
        let inner = '';
        for (const a of imgs) {
            inner += `<img class="post-image" data-cid="${a.cid}" data-post-id="${post.id}" data-mime="${escapeHtml(a.mimeType)}" alt="attachment" />`;
        }
        for (const a of vids) {
            inner += `<div class="video-wrap">
                <video class="post-video" data-cid="${a.cid}" data-post-id="${post.id}" data-mime="${escapeHtml(a.mimeType)}" controls preload="none"></video>
                <div class="video-controls"><button class="video-download" data-cid="${a.cid}" data-mime="${escapeHtml(a.mimeType)}" title="Download video">Download</button></div>
            </div>`;
        }
        for (const a of auds) {
            inner += `<audio class="post-audio" data-cid="${a.cid}" data-post-id="${post.id}" data-mime="${escapeHtml(a.mimeType)}" controls preload="none"></audio>`;
        }
        for (const a of others) {
            const sizeKb = Math.round(a.sizeBytes / 1024);
            const ext = a.mimeType.split('/').pop() || 'file';
            inner += `<button class="post-file file-download" data-cid="${a.cid}" data-post-id="${post.id}" data-mime="${escapeHtml(a.mimeType)}" data-ext="${escapeHtml(ext)}" title="Download">${escapeHtml(ext.toUpperCase())} (${sizeKb} KB)</button>`;
        }
        attachmentsHtml = `<div class="post-attachments">${inner}</div>`;
    }

    const authorLink = post.isMe
        ? `<span class="${authorClass}">${escapeHtml(authorName)}${meTag}</span>`
        : `<a class="post-author-link" data-node-id="${post.author}" title="View in People tab">${escapeHtml(authorName)}</a>`;

    // Engagement bar: reactions (top 5 + total) + comments
    let reactionsHtml = '';
    if (post.reactionCounts && post.reactionCounts.length > 0) {
        const sorted = [...post.reactionCounts].sort((a, b) => b.count - a.count);
        const top5 = sorted.slice(0, 5);
        const totalResponses = sorted.reduce((sum, r) => sum + r.count, 0);
        const pills = top5.map(r =>
            `<button class="reaction-pill${r.reactedByMe ? ' reacted' : ''}" data-post-id="${post.id}" data-emoji="${escapeHtml(r.emoji)}">${r.emoji} ${r.count}</button>`
        ).join('');
        const summary = totalResponses > 0 ? `<span class="reaction-summary">${totalResponses} response${totalResponses !== 1 ? 's' : ''}</span>` : '';
        reactionsHtml = pills + summary;
    }

    const commentCount = post.commentCount || 0;
    const shareBtn = post.visibility === 'public'
        ? `<button class="share-btn" data-post-id="${post.id}" title="Copy share link">Share</button>`
        : '';
    const engagementBar = `<div class="engagement-bar">
        <div class="reaction-pills">${reactionsHtml}<button class="react-btn" data-post-id="${post.id}" title="React">\u263A</button></div>
        <div class="engagement-right"><button class="comment-toggle-btn" data-post-id="${post.id}">Comment${commentCount > 0 ? ` (${commentCount})` : ''}</button>${shareBtn}</div>
    </div>`;

    return `<div class="post" style="animation-delay: ${delay}s" data-post-id="${post.id}">
        <div class="post-meta">
            <span class="post-author">${icon}${authorLink}${visBadge}</span>
            <span class="post-time" title="${new Date(post.timestampMs).toLocaleString()}">${timeStr}</span>
            ${deleteBtn}
        </div>
        <div class="post-content">${displayContent}</div>
        ${attachmentsHtml}
        ${engagementBar}
        <div class="comment-thread hidden" id="comments-${post.id}"></div>
        <div class="post-id">${post.id.substring(0, 16)}</div>
    </div>`;
}

// Render a message post (similar to renderPost but with follow action for requests)
function renderMessage(post, index, showFollowBtn) {
    const html = renderPost(post, index);
    if (!showFollowBtn) return html;
    // Insert a follow button after the post
    return html + `<div class="msg-request-action">
        <button class="btn btn-primary btn-sm follow-btn" data-node-id="${post.author}">Follow to accept</button>
    </div>`;
}

function renderEmptyState(message, hint) {
    return `<div class="empty-state">
        <div class="empty-state-icon"></div>
        <p class="empty-state-msg">${escapeHtml(message)}</p>
        ${hint ? `<p class="empty-state-hint">${escapeHtml(hint)}</p>` : ''}
    </div>`;
}

function renderLoading() {
    return '<div class="loading-state"><div class="loading-dots"><span></span><span></span><span></span></div></div>';
}

function updateTabBadge(tabName, count) {
    const tab = document.querySelector(`.tab[data-tab="${tabName}"]`);
    if (!tab) return;
    let badge = tab.querySelector('.tab-badge');
    if (count > 0) {
        if (!badge) {
            badge = document.createElement('span');
            badge.className = 'tab-badge';
            tab.appendChild(badge);
        }
        badge.textContent = count;
    } else if (badge) {
        badge.remove();
    }
}

// --- Compose auto-grow ---
function autoGrow(el) {
    el.style.height = 'auto';
    el.style.height = Math.max(el.scrollHeight, 60) + 'px';
}

function updateCharCount() {
    const len = postContent.value.length;
    charCount.textContent = `${len}/${POST_MAX_CHARS}`;
    charCount.classList.toggle('char-warn', len > POST_MAX_CHARS * 0.9);
    charCount.classList.toggle('char-over', len >= POST_MAX_CHARS);
}

// --- Data loaders ---
async function loadNodeInfo() {
    try {
        const info = await invoke('get_node_info');
        myNodeId = info.nodeId;
        const label = info.displayName || info.nodeId.substring(0, 16) + '...';
        const icon = generateIdenticon(info.nodeId, 18);
        nodeIdEl.innerHTML = icon + ' <span>' + escapeHtml(label) + '</span>';
        nodeIdEl.title = info.nodeId;
        connectString = info.connectString;
        // Show connect string and QR code
        const csDisplay = $('#connect-string-display');
        const qrContainer = $('#qr-code');
        if (csDisplay && connectString) {
            csDisplay.textContent = connectString;
        }
        if (qrContainer && connectString) {
            try {
                qrContainer.innerHTML = generateQRCodeSVG(connectString, 200);
            } catch (qrErr) {
                qrContainer.innerHTML = '<span class="empty-hint">QR code unavailable</span>';
            }
        }
        // Pre-fill profile editor with current values
        if (info.displayName && !profileNameInput.dataset.touched) {
            profileNameInput.value = info.displayName;
        }
        return info;
    } catch (e) {
        nodeIdEl.textContent = 'error';
        console.error('loadNodeInfo:', e);
        return null;
    }
}

async function loadStats() {
    try {
        const s = await invoke('get_stats');
        statPosts.textContent = `${s.postCount} posts`;
        statFollows.textContent = `${s.followCount} following`;
        // Show people count as follows + audience, with "+" if more discoverable peers exist
        const peopleCount = s.followCount;
        const hasMore = s.peerCount > s.followCount;
        statPeers.textContent = `${peopleCount}${hasMore ? '+' : ''} people`;
        updateTabBadge('people', peopleCount);
    } catch (e) {
        console.error('loadStats:', e);
    }
}

async function loadFeed(force) {
    try {
        const allPosts = await invoke('get_feed');
        const posts = allPosts.filter(p => p.visibility !== 'encrypted-for-me');
        // Fingerprint: post IDs + reaction counts + comment counts
        const fp = posts.map(p => `${p.id}:${(p.reactionCounts||[]).map(r=>r.emoji+r.count).join(',')}:${p.commentCount||0}`).join('|');
        if (!force && fp === _feedFingerprint) return;
        _feedFingerprint = fp;
        // Preserve expanded comment threads
        const expandedComments = new Set();
        feedList.querySelectorAll('.comment-thread:not(.hidden)').forEach(el => {
            const postEl = el.closest('.post');
            if (postEl) expandedComments.add(postEl.dataset.postId);
        });
        if (posts.length === 0) {
            feedList.innerHTML = renderEmptyState(
                'Your feed is empty',
                'Follow peers on the People tab to see their posts here.'
            );
        } else {
            feedList.innerHTML = posts.map(renderPost).join('');
            loadPostMedia(feedList);
            // Restore expanded comment threads
            for (const postId of expandedComments) {
                const thread = feedList.querySelector(`#comments-${postId}`);
                if (thread) {
                    thread.classList.remove('hidden');
                    loadCommentThread(postId, thread);
                }
            }
        }
    } catch (e) {
        feedList.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

async function loadMyPosts(force) {
    try {
        const posts = await invoke('get_all_posts');
        const mine = posts.filter(p => p.isMe && p.visibility !== 'encrypted-for-me');
        const fp = mine.map(p => `${p.id}:${(p.reactionCounts||[]).map(r=>r.emoji+r.count).join(',')}:${p.commentCount||0}`).join('|');
        if (!force && fp === _myPostsFingerprint) return;
        _myPostsFingerprint = fp;
        const expandedComments = new Set();
        myPostsList.querySelectorAll('.comment-thread:not(.hidden)').forEach(el => {
            const postEl = el.closest('.post');
            if (postEl) expandedComments.add(postEl.dataset.postId);
        });
        if (mine.length === 0) {
            myPostsList.innerHTML = renderEmptyState(
                'No posts yet',
                'Write your first post above!'
            );
        } else {
            myPostsList.innerHTML = mine.map(renderPost).join('');
            loadPostMedia(myPostsList);
            for (const postId of expandedComments) {
                const thread = myPostsList.querySelector(`#comments-${postId}`);
                if (thread) {
                    thread.classList.remove('hidden');
                    loadCommentThread(postId, thread);
                }
            }
        }
    } catch (e) {
        myPostsList.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

async function loadMessages(force) {
    try {
        const [posts, follows] = await Promise.all([
            invoke('get_all_posts'),
            invoke('list_follows'),
        ]);
        const followSet = new Set(follows.map(f => f.nodeId));

        // Collect DMs: received encrypted-for-me OR my sent encrypted with recipients
        const dms = posts.filter(p => {
            if (!p.isMe && p.visibility === 'encrypted-for-me') return true;
            if (p.isMe && p.recipients && p.recipients.length > 0) return true;
            return false;
        });

        // Separate message requests (from non-followed) vs conversations (from followed or sent by me)
        const requests = dms.filter(p => !p.isMe && !followSet.has(p.author));
        const convMessages = dms.filter(p => p.isMe || followSet.has(p.author));

        // Group conversation messages by partner
        const threads = new Map(); // partnerNodeId → { posts: [], partnerName: string|null }
        for (const p of convMessages) {
            let partner;
            if (p.isMe) {
                // Sent DM — partner is first recipient that isn't me
                partner = p.recipients.find(r => r !== myNodeId) || p.recipients[0];
            } else {
                partner = p.author;
            }
            if (!partner) continue;
            if (!threads.has(partner)) {
                threads.set(partner, { posts: [], partnerName: null });
            }
            threads.get(partner).posts.push(p);
        }

        // Resolve partner names + sort threads by most recent message
        for (const [partnerId, thread] of threads) {
            thread.posts.sort((a, b) => a.timestampMs - b.timestampMs);
            // Get partner name from existing post data
            const partnerPost = thread.posts.find(p => p.author === partnerId);
            thread.partnerName = partnerPost ? partnerPost.authorName : null;
            if (!thread.partnerName) {
                const follow = follows.find(f => f.nodeId === partnerId);
                if (follow) thread.partnerName = follow.displayName;
            }
        }

        const sortedThreads = [...threads.entries()].sort((a, b) => {
            const aLast = a[1].posts[a[1].posts.length - 1].timestampMs;
            const bLast = b[1].posts[b[1].posts.length - 1].timestampMs;
            return bLast - aLast;
        });

        // Track newest message timestamp for tiered polling
        if (sortedThreads.length > 0) {
            _lastMsgTimestamp = sortedThreads[0][1].posts[sortedThreads[0][1].posts.length - 1].timestampMs;
        }

        // Fingerprint: partner IDs + message counts + last timestamp
        const fp = sortedThreads.map(([pid, t]) => `${pid}:${t.posts.length}:${t.posts[t.posts.length-1].timestampMs}`).join('|')
            + '|req:' + requests.map(p => p.id).join(',');
        if (!force && fp === _messagesFingerprint) return;
        _messagesFingerprint = fp;

        // Notify on new incoming messages
        try {
            const notifMsg = await invoke('get_setting', { key: 'notif_messages' }).catch(() => null) || 'on';
            if (notifMsg !== 'off') {
                for (const [partnerId, thread] of sortedThreads) {
                    for (const p of thread.posts) {
                        if (p.isMe || _notifiedMessages.has(p.id)) continue;
                        _notifiedMessages.add(p.id);
                        if (_notifiedMessages.size > 1) { // skip first load
                            const name = thread.partnerName || partnerId.slice(0, 8);
                            const body = notifMsg === 'preview' ? (p.decryptedContent || '').slice(0, 100) : 'New message';
                            maybeNotify(`Message from ${name}`, body, `msg-${p.id}`);
                        }
                    }
                }
            }
        } catch (_) {}

        // Preserve expanded conversation state
        const expandedPartner = conversationsList.querySelector('.conversation-active')?.dataset?.partner || null;

        // Render conversation list
        if (sortedThreads.length === 0) {
            conversationsList.innerHTML = `<div class="section-card">${renderEmptyState(
                'No conversations yet',
                'Send a DM above to start a conversation.'
            )}</div>`;
        } else {
            conversationsList.innerHTML = sortedThreads.map(([partnerId, thread]) => {
                const lastMsg = thread.posts[thread.posts.length - 1];
                const icon = generateIdenticon(partnerId, 22);
                const name = escapeHtml(peerLabel(partnerId, thread.partnerName));
                const time = relativeTime(lastMsg.timestampMs);
                const preview = lastMsg.decryptedContent || lastMsg.content || '';
                const previewText = escapeHtml(preview.length > 80 ? preview.substring(0, 80) + '...' : preview);
                const prefix = lastMsg.isMe ? '<span class="conv-you">You: </span>' : '';

                const messagesHtml = thread.posts.map(p => {
                    const content = p.decryptedContent || p.content || '';
                    const msgTime = relativeTime(p.timestampMs);
                    const side = p.isMe ? 'chat-mine' : 'chat-theirs';
                    return `<div class="chat-bubble ${side}">
                        <div class="chat-text">${escapeHtml(content)}</div>
                        <div class="chat-time">${msgTime}</div>
                    </div>`;
                }).join('');

                return `<div class="conversation-item section-card" data-partner="${partnerId}">
                    <div class="conv-header">
                        <div class="conv-header-left">${icon} <span class="conv-name">${name}</span></div>
                        <span class="conv-time">${time}</span>
                    </div>
                    <div class="conv-preview">${prefix}${previewText}</div>
                    <div class="conversation-messages hidden">
                        <div class="chat-window">${messagesHtml}</div>
                        <div class="conv-reply">
                            <textarea class="conv-reply-input" placeholder="Reply..." rows="2"></textarea>
                            <button class="btn btn-primary btn-sm conv-reply-btn" data-partner="${partnerId}">Send</button>
                        </div>
                    </div>
                </div>`;
            }).join('');

            // Open conversation in popover on click
            conversationsList.querySelectorAll('.conversation-item').forEach(item => {
                item.querySelector('.conv-header').addEventListener('click', () => {
                    const partnerId = item.dataset.partner;
                    const msgsHtml = item.querySelector('.chat-window').innerHTML;
                    const partnerName = item.querySelector('.conv-name').textContent;

                    openPopover(partnerName, `
                        <div class="chat-window" style="max-height:55vh;overflow-y:auto;display:flex;flex-direction:column;gap:0.35rem;padding:0.5rem 0">${msgsHtml}</div>
                        <div class="conv-reply" style="display:flex;gap:0.4rem;align-items:flex-end;margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid #2a2a40">
                            <textarea class="conv-reply-input" id="popover-reply-input" placeholder="Reply..." rows="2" style="flex:1;padding:0.4rem;background:#1a1a2e;color:#e0e0e0;border:1px solid #333;border-radius:4px;resize:none;font-family:inherit;font-size:0.85rem;min-height:36px;line-height:1.4"></textarea>
                            <button class="btn btn-primary btn-sm" id="popover-reply-btn">Send</button>
                        </div>
                    `, {
                        onOpen() {
                            // Scroll chat to bottom
                            const chatWindow = $('#popover-body .chat-window');
                            if (chatWindow) chatWindow.scrollTop = chatWindow.scrollHeight;
                            // Focus reply
                            const input = $('#popover-reply-input');
                            if (input) setTimeout(() => input.focus(), 100);
                            // Wire send
                            const sendReply = async () => {
                                const content = input.value.trim();
                                if (!content) return;
                                $('#popover-reply-btn').disabled = true;
                                try {
                                    await invoke('create_post', { content, visibility: 'direct', recipientHex: partnerId });
                                    input.value = '';
                                    toast('Reply sent!');
                                    closePopover();
                                    loadMessages(true);
                                } catch (e) {
                                    toast('Error: ' + e);
                                } finally {
                                    const btn = $('#popover-reply-btn');
                                    if (btn) btn.disabled = false;
                                }
                            };
                            $('#popover-reply-btn').addEventListener('click', sendReply);
                            input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.ctrlKey) sendReply(); });
                        }
                    });
                });
            });

            // Conversations open in popovers now — no inline restore needed
        }

        // Message requests from non-followed
        if (requests.length === 0) {
            messageRequestsList.innerHTML = `<p class="empty-hint">No pending requests</p>`;
        } else {
            messageRequestsList.innerHTML = requests.map((p, i) => renderMessage(p, i, true)).join('');
            attachFollowHandlers(messageRequestsList);
        }

        updateTabBadge('messages', requests.length);
    } catch (e) {
        conversationsList.innerHTML = `<div class="section-card"><p class="status-err">Error: ${e}</p></div>`;
    }
}

async function loadDmRecipientOptions() {
    try {
        const [follows, peers] = await Promise.all([
            invoke('list_follows'),
            invoke('list_peers'),
        ]);
        const followSet = new Set(follows.map(f => f.nodeId));
        const otherPeers = peers.filter(p => !followSet.has(p.nodeId) && p.nodeId !== myNodeId);

        let html = '<option value="">(select recipient)</option>';
        if (follows.length > 0) {
            html += '<optgroup label="Following">';
            html += follows.map(f => {
                const label = f.displayName || f.nodeId.substring(0, 12) + '...';
                return `<option value="${f.nodeId}">${escapeHtml(label)}</option>`;
            }).join('');
            html += '</optgroup>';
        }
        if (otherPeers.length > 0) {
            html += '<optgroup label="Other Peers">';
            html += otherPeers.map(p => {
                const label = p.displayName || p.nodeId.substring(0, 12) + '...';
                return `<option value="${p.nodeId}">${escapeHtml(label)}</option>`;
            }).join('');
            html += '</optgroup>';
        }
        dmRecipientSelect.innerHTML = html;
    } catch (e) {
        dmRecipientSelect.innerHTML = '<option value="">Error loading</option>';
    }
}

async function loadPeers() {
    if (!peersList) return;
    try {
        const [peers, follows] = await Promise.all([
            invoke('list_peers'),
            invoke('list_follows'),
        ]);
        const followSet = new Set(follows.map(f => f.nodeId));

        if (peers.length === 0) {
            peersList.innerHTML = renderEmptyState('No known peers', 'Connect to a peer using their connect string, or wait for mDNS discovery.');
        } else {
            // Sort by reach level, then by last_seen within each level
            const reachOrder = { mesh: 0, n1: 1, n2: 2, n3: 3, known: 4 };
            const sorted = [...peers].sort((a, b) => {
                const ra = reachOrder[a.reach] ?? 4;
                const rb = reachOrder[b.reach] ?? 4;
                if (ra !== rb) return ra - rb;
                return (b.lastSeen || 0) - (a.lastSeen || 0);
            });

            peersList.innerHTML = sorted.map(p => {
                const label = escapeHtml(peerLabel(p.nodeId, p.displayName));
                const icon = generateIdenticon(p.nodeId, 18);
                const anchor = p.isAnchor ? '<span class="anchor-badge">anchor</span>' : '';
                const intro = p.introducedBy ? `<span class="intro-tag">via ${escapeHtml(p.introducedBy)}</span>` : '';
                const addr = p.addresses.length > 0 ? `<span class="peer-addr">${escapeHtml(p.addresses[0])}</span>` : '';
                const seen = p.lastSeen ? `<span class="peer-seen">${relativeTime(p.lastSeen)}</span>` : '';

                let reachBadge = '';
                if (p.reach === 'mesh') reachBadge = '<span class="reach-badge reach-mesh">Mesh</span>';
                else if (p.reach === 'n1') reachBadge = '<span class="reach-badge reach-n1">N1</span>';
                else if (p.reach === 'n2') reachBadge = '<span class="reach-badge reach-n2">N2</span>';
                else if (p.reach === 'n3') reachBadge = '<span class="reach-badge reach-n3">N3</span>';

                let actions = '';
                if (p.nodeId === myNodeId) {
                    actions = '<span class="self-tag">(you)</span>';
                } else {
                    const msgBtn = `<button class="btn btn-ghost btn-sm msg-peer-btn" data-node-id="${p.nodeId}" title="Send message">msg</button>`;
                    const followBtn = followSet.has(p.nodeId)
                        ? `<button class="btn btn-ghost btn-sm unfollow-btn" data-node-id="${p.nodeId}">Unfollow</button>`
                        : `<button class="btn btn-primary btn-sm follow-btn" data-node-id="${p.nodeId}">Follow</button>`;
                    actions = `${msgBtn} ${followBtn}`;
                }

                return `<div class="peer-card" data-node-id="${p.nodeId}">
                    <div class="peer-card-row">${icon} <a class="peer-name-link" data-node-id="${p.nodeId}">${label}</a> ${reachBadge} ${anchor}</div>
                    <div class="peer-card-bio"></div>
                    <div class="peer-card-meta">${intro} ${addr} ${seen}</div>
                    <div class="peer-card-actions">${actions}</div>
                </div>`;
            }).join('');

            attachFollowHandlers(peersList);
            // Lazy-load bios
            loadPeerBios(peersList);
        }
    } catch (e) {
        peersList.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

async function loadPeerBios(container) {
    const cards = container.querySelectorAll('[data-node-id]');
    for (const card of cards) {
        const nodeId = card.dataset.nodeId;
        if (!nodeId || nodeId === myNodeId) continue;
        try {
            const info = await invoke('resolve_display', { nodeIdHex: nodeId });
            const bioEl = card.querySelector('.peer-card-bio');
            if (bioEl && info.bio) {
                bioEl.textContent = info.bio;
                bioEl.classList.add('peer-bio');
            }
        } catch (e) { /* ignore — peer may not have profile */ }
    }
}

async function loadFollows() {
    try {
        const [follows, outbound, inbound] = await Promise.all([
            invoke('list_follows'),
            invoke('list_audience_outbound'),
            invoke('list_audience'),
        ]);
        const outboundSet = new Set(outbound.map(r => r.nodeId));
        const approvedSet = new Set(outbound.filter(r => r.status === 'approved').map(r => r.nodeId));
        const inboundApprovedSet = new Set(inbound.filter(r => r.status === 'approved').map(r => r.nodeId));

        if (follows.length === 0) {
            followsList.innerHTML = `<div>${renderEmptyState('Not following anyone', 'Follow suggested peers or connect manually.')}</div>`;
        } else {
            const now = Date.now();
            const ONLINE_THRESHOLD = 5 * 60 * 1000; // 5 minutes
            const renderFollowCard = (f) => {
                const icon = generateIdenticon(f.nodeId, 18);
                const label = escapeHtml(peerLabel(f.nodeId, f.displayName));
                const isSelf = f.nodeId === myNodeId;

                let audienceBadge = '';
                let mutualBadge = '';
                let lastSeenHtml = '';
                let actions = '';
                if (isSelf) {
                    actions = '<span class="self-tag">(you)</span>';
                } else {
                    if (inboundApprovedSet.has(f.nodeId)) {
                        mutualBadge = '<span class="mutual-badge">mutual</span>';
                    }
                    if (approvedSet.has(f.nodeId)) {
                        audienceBadge = '<span class="audience-badge">audience</span>';
                    } else if (outboundSet.has(f.nodeId)) {
                        audienceBadge = '<span class="audience-badge pending">requested</span>';
                    }
                    if (!f.isOnline && f.lastActivityMs > 0) {
                        lastSeenHtml = `<span class="last-seen">Last online: ${formatTimeAgo(f.lastActivityMs)}</span>`;
                    }
                    const audienceBtn = !approvedSet.has(f.nodeId) && !outboundSet.has(f.nodeId)
                        ? `<button class="btn btn-ghost btn-sm request-audience-btn" data-node-id="${f.nodeId}">Ask to join audience</button>`
                        : '';
                    const syncBtn = `<button class="btn btn-ghost btn-sm sync-peer-btn" data-node-id="${f.nodeId}" title="Sync posts from this peer">Sync</button>`;
                    const msgBtn = `<button class="btn btn-ghost btn-sm msg-peer-btn" data-node-id="${f.nodeId}" title="Send message">msg</button>`;
                    const unfollowBtn = `<button class="btn btn-ghost btn-sm unfollow-btn" data-node-id="${f.nodeId}">Unfollow</button>`;
                    actions = `${audienceBtn} ${syncBtn} ${msgBtn} ${unfollowBtn}`;
                }
                return `<div class="peer-card" data-node-id="${f.nodeId}">
                    <div class="peer-card-row">${icon} <a class="peer-name-link" data-node-id="${f.nodeId}">${label}</a> ${mutualBadge} ${audienceBadge}</div>
                    ${lastSeenHtml ? `<div class="peer-card-lastseen">${lastSeenHtml}</div>` : ''}
                    <div class="peer-card-bio"></div>
                    <div class="peer-card-actions">${actions}</div>
                </div>`;
            };

            const online = follows.filter(f => f.isOnline || (f.lastActivityMs > 0 && (now - f.lastActivityMs) < ONLINE_THRESHOLD));
            const offline = follows.filter(f => !online.includes(f));

            let html = '';
            if (online.length > 0) {
                html += `<div class="follows-section-header">Following: Online (${online.length})</div>`;
                html += online.map(renderFollowCard).join('');
            }
            if (offline.length > 0) {
                html += `<div class="follows-section-header">Following: Offline (${offline.length})</div>`;
                html += offline.map(renderFollowCard).join('');
            }
            followsList.innerHTML = html;

            // Attach unfollow handlers
            followsList.querySelectorAll('.unfollow-btn').forEach(btn => {
                btn.addEventListener('click', async () => {
                    btn.disabled = true;
                    try {
                        await invoke('unfollow_node', { nodeIdHex: btn.dataset.nodeId });
                        toast('Unfollowed');
                        loadFollows();

                        loadStats();
                        loadFeed(true);
                    } catch (e) {
                        toast('Error: ' + e);
                    } finally {
                        btn.disabled = false;
                    }
                });
            });

            // Attach per-peer sync handlers
            followsList.querySelectorAll('.sync-peer-btn').forEach(btn => {
                btn.addEventListener('click', async () => {
                    btn.disabled = true;
                    btn.textContent = 'Syncing...';
                    try {
                        await invoke('sync_from_peer', { nodeIdHex: btn.dataset.nodeId });
                        toast('Sync complete!');
                        loadFeed(true);
                        loadMyPosts(true);
                    } catch (e) {
                        toast('Error: ' + e);
                    } finally {
                        btn.disabled = false;
                        btn.textContent = 'Sync';
                    }
                });
            });

            // Attach audience request handlers
            followsList.querySelectorAll('.request-audience-btn').forEach(btn => {
                btn.addEventListener('click', async () => {
                    btn.disabled = true;
                    try {
                        await invoke('request_audience', { nodeIdHex: btn.dataset.nodeId });
                        toast('Audience request sent!');
                        loadFollows();
                    } catch (e) {
                        toast('Error: ' + e);
                    } finally {
                        btn.disabled = false;
                    }
                });
            });

            // Lazy-load bios
            loadPeerBios(followsList);
        }
    } catch (e) {
        followsList.innerHTML = `<div class="status-err">Error: ${e}</div>`;
    }
}

// loadSuggested removed — Suggested Peers section removed from UI

async function loadDiscoverPeople() {
    const container = $('#discover-list');
    try {
        const [peers, follows] = await Promise.all([
            invoke('list_peers'),
            invoke('list_follows'),
        ]);
        const followSet = new Set(follows.map(f => f.nodeId));

        // Filter: has display name, not already followed, not self
        const discoverable = peers.filter(p =>
            p.displayName && !followSet.has(p.nodeId) && p.nodeId !== myNodeId
        );

        if (discoverable.length === 0) {
            container.innerHTML = renderEmptyState(
                'No new people found',
                'Connect to more peers to discover people on the network.'
            );
        } else {
            container.innerHTML = discoverable.map(p => {
                const icon = generateIdenticon(p.nodeId, 18);
                const label = escapeHtml(p.displayName);
                let reachBadge = '';
                if (p.reach === 'mesh') reachBadge = '<span class="reach-badge reach-mesh">Mesh</span>';
                else if (p.reach === 'n1') reachBadge = '<span class="reach-badge reach-n1">N1</span>';
                else if (p.reach === 'n2') reachBadge = '<span class="reach-badge reach-n2">N2</span>';
                else if (p.reach === 'n3') reachBadge = '<span class="reach-badge reach-n3">N3</span>';

                return `<div class="peer-card" data-node-id="${p.nodeId}">
                    <div class="peer-card-row">${icon} ${label} ${reachBadge}</div>
                    <div class="peer-card-bio"></div>
                    <div class="peer-card-actions">
                        <button class="btn btn-primary btn-sm follow-btn" data-node-id="${p.nodeId}">Follow</button>
                        <button class="btn btn-ghost btn-sm msg-peer-btn" data-node-id="${p.nodeId}" title="Send message">msg</button>
                    </div>
                </div>`;
            }).join('');

            attachFollowHandlers(container);
            loadPeerBios(container);
        }
    } catch (e) {
        container.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

// Shared handler for follow/unfollow buttons in a container
function attachFollowHandlers(container) {
    container.querySelectorAll('.follow-btn').forEach(btn => {
        btn.addEventListener('click', async () => {
            btn.disabled = true;
            try {
                await invoke('follow_node', { nodeIdHex: btn.dataset.nodeId });
                toast('Followed! Syncing posts...');
                loadFollows();
                loadStats();
                loadFeed(true);
                if (currentTab === 'messages') loadMessages(true);
                // Auto-sync triggers in backend; refresh feed again after a delay
                setTimeout(() => loadFeed(true), 3000);
            } catch (e) {
                toast('Error: ' + e);
            } finally {
                btn.disabled = false;
            }
        });
    });
    container.querySelectorAll('.unfollow-btn').forEach(btn => {
        btn.addEventListener('click', async () => {
            btn.disabled = true;
            try {
                await invoke('unfollow_node', { nodeIdHex: btn.dataset.nodeId });
                toast('Unfollowed');
                loadFollows();
                loadStats();
                loadFeed(true);
            } catch (e) {
                toast('Error: ' + e);
            } finally {
                btn.disabled = false;
            }
        });
    });
}

async function loadRedundancy() {
    try {
        const r = await invoke('get_redundancy_info');
        const zeroClass = r.zeroReplicas > 0 ? 'warn' : 'ok';
        const oneClass = r.oneReplica > 0 ? '' : 'ok';
        redundancyPanel.innerHTML = `<div class="redundancy-grid">
            <div class="redundancy-item ${zeroClass}">
                <div class="redundancy-value">${r.zeroReplicas}</div>
                <div class="redundancy-label">Unreplicated</div>
            </div>
            <div class="redundancy-item ${oneClass}">
                <div class="redundancy-value">${r.oneReplica}</div>
                <div class="redundancy-label">1 replica</div>
            </div>
            <div class="redundancy-item ok">
                <div class="redundancy-value">${r.twoPlusReplicas}</div>
                <div class="redundancy-label">2+ replicas</div>
            </div>
            <div class="redundancy-item">
                <div class="redundancy-value">${r.total}</div>
                <div class="redundancy-label">Total posts</div>
            </div>
        </div>`;
    } catch (e) {
        redundancyPanel.innerHTML = `<p class="empty-hint">Could not load redundancy info</p>`;
    }
}

// --- Audience management ---
async function loadAudience() {
    try {
        const records = await invoke('list_audience');
        const pending = records.filter(r => r.status === 'pending');
        const approved = records.filter(r => r.status === 'approved');

        if (pending.length === 0) {
            audiencePendingList.innerHTML = '<p class="empty-hint">No pending requests</p>';
        } else {
            audiencePendingList.innerHTML = pending.map(r => {
                const label = escapeHtml(peerLabel(r.nodeId, r.displayName));
                const icon = generateIdenticon(r.nodeId, 18);
                return `<div class="peer-card">
                    <div class="peer-card-row">${icon} ${label}</div>
                    <div class="peer-card-meta"><span>${relativeTime(r.requestedAt)}</span></div>
                    <div class="peer-card-actions">
                        <button class="btn btn-primary btn-sm approve-audience-btn" data-node-id="${r.nodeId}">Approve</button>
                        <button class="btn btn-danger btn-sm deny-audience-btn" data-node-id="${r.nodeId}">Deny</button>
                    </div>
                </div>`;
            }).join('');

            audiencePendingList.querySelectorAll('.approve-audience-btn').forEach(btn => {
                btn.addEventListener('click', async () => {
                    btn.disabled = true;
                    try {
                        await invoke('approve_audience', { nodeIdHex: btn.dataset.nodeId });
                        toast('Audience approved');
                        loadAudience();
                    } catch (e) { toast('Error: ' + e); }
                });
            });
            audiencePendingList.querySelectorAll('.deny-audience-btn').forEach(btn => {
                btn.addEventListener('click', async () => {
                    btn.disabled = true;
                    try {
                        await invoke('remove_audience', { nodeIdHex: btn.dataset.nodeId });
                        toast('Audience denied');
                        loadAudience();
                    } catch (e) { toast('Error: ' + e); }
                });
            });
        }

        if (approved.length === 0) {
            audienceApprovedList.innerHTML = '<p class="empty-hint">No approved audience members</p>';
        } else {
            audienceApprovedList.innerHTML = approved.map(r => {
                const label = escapeHtml(peerLabel(r.nodeId, r.displayName));
                const icon = generateIdenticon(r.nodeId, 18);
                return `<div class="peer-card">
                    <div class="peer-card-row">${icon} ${label}</div>
                    <div class="peer-card-meta"><span>Approved ${r.approvedAt ? relativeTime(r.approvedAt) : ''}</span></div>
                    <div class="peer-card-actions">
                        <button class="btn btn-danger btn-sm remove-audience-btn" data-node-id="${r.nodeId}">Remove</button>
                    </div>
                </div>`;
            }).join('');

            audienceApprovedList.querySelectorAll('.remove-audience-btn').forEach(btn => {
                btn.addEventListener('click', async () => {
                    if (!confirm('Remove this audience member?')) return;
                    btn.disabled = true;
                    try {
                        await invoke('remove_audience', { nodeIdHex: btn.dataset.nodeId });
                        toast('Audience member removed');
                        loadAudience();
                    } catch (e) { toast('Error: ' + e); }
                });
            });
        }
    } catch (e) {
        audiencePendingList.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

// --- Network diagnostics ---
async function loadNetworkSummary() {
    if (!networkSummaryEl) return;
    try {
        const s = await invoke('get_network_summary');
        networkSummaryEl.innerHTML = `<div class="diag-grid">
            <div class="diag-item"><span class="diag-value">${s.totalConnections}</span><span class="diag-label">Connections</span></div>
            <div class="diag-item"><span class="diag-value">${s.preferredCount}</span><span class="diag-label">Preferred</span></div>
            <div class="diag-item"><span class="diag-value">${s.localCount}</span><span class="diag-label">Local</span></div>
            <div class="diag-item"><span class="diag-value">${s.wideCount}</span><span class="diag-label">Wide</span></div>
            <div class="diag-item"><span class="diag-value">${s.n2Distinct}</span><span class="diag-label">N2 Reach</span></div>
            <div class="diag-item"><span class="diag-value">${s.n3Distinct}</span><span class="diag-label">N3 Reach</span></div>
        </div>`;
    } catch (e) {
        networkSummaryEl.innerHTML = `<p class="empty-hint">Could not load network summary</p>`;
    }
}

async function loadConnections() {
    if (!connectionsList) return;
    try {
        const conns = await invoke('list_connections');
        if (conns.length === 0) {
            connectionsList.innerHTML = '<p class="empty-hint">No active mesh connections</p>';
        } else {
            connectionsList.innerHTML = conns.map(c => {
                const label = escapeHtml(peerLabel(c.nodeId, c.displayName));
                const icon = generateIdenticon(c.nodeId, 18);
                const slotClass = c.slotKind === 'Preferred' ? 'slot-preferred'
                    : c.slotKind === 'Wide' ? 'slot-wide' : 'slot-local';
                const duration = c.connectedAt ? relativeTime(c.connectedAt) : '';
                return `<div class="peer-card">
                    <div class="peer-card-row">${icon} ${label} <span class="slot-badge ${slotClass}">${c.slotKind}</span></div>
                    <div class="peer-card-meta"><span>${duration}</span></div>
                </div>`;
            }).join('');
        }
    } catch (e) {
        connectionsList.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

async function loadAllDiagnostics() {
    await Promise.all([loadNetworkSummary(), loadConnections(), loadPeers(), loadActivityLog()]);
    lastDiagUpdate = Date.now();
    const ts = $('#diag-update-time');
    if (ts) ts.textContent = 'Updated ' + relativeTime(lastDiagUpdate);
}

let activityInterval = null;

async function loadActivityLog() {
    try {
        const data = await invoke('get_activity_log');
        // Render timers
        const timersEl = $('#activity-timers');
        if (timersEl) {
            const now = Date.now();
            timersEl.innerHTML = renderTimer('Rebalance', data.rebalanceLastMs, data.rebalanceIntervalSecs, now)
                + renderTimer('Anchor Register', data.anchorRegisterLastMs, data.anchorRegisterIntervalSecs, now);
        }
        // Render events (newest first)
        const logEl = $('#activity-log');
        if (logEl) {
            if (!data.events || data.events.length === 0) {
                logEl.innerHTML = '<div class="activity-empty">No activity yet</div>';
            } else {
                const reversed = [...data.events].reverse();
                logEl.innerHTML = reversed.map(e => {
                    const t = new Date(e.timestampMs);
                    const time = t.toTimeString().slice(0, 8);
                    const peer = e.peerId ? e.peerId.slice(0, 8) : '';
                    const fullId = e.peerId || '';
                    return `<div class="activity-entry level-${e.level}">
                        <span class="activity-time">${time}</span>
                        <span class="activity-badge badge-${e.category}">${e.category}</span>
                        <span class="activity-msg">${escapeHtml(e.message)}</span>
                        ${peer ? `<span class="activity-peer" data-full-id="${fullId}">${peer}</span>` : ''}
                    </div>`;
                }).join('');
                // Attach click handlers for peer IDs
                logEl.querySelectorAll('.activity-peer[data-full-id]').forEach(span => {
                    span.addEventListener('click', () => {
                        const expanded = span.parentElement.querySelector('.activity-peer-expanded');
                        if (expanded) { expanded.remove(); return; }
                        const div = document.createElement('div');
                        div.className = 'activity-peer-expanded';
                        div.textContent = span.dataset.fullId;
                        span.parentElement.appendChild(div);
                    });
                });
            }
        }
    } catch (e) {
        const logEl = $('#activity-log');
        if (logEl) logEl.innerHTML = `<div class="activity-empty">Error: ${e}</div>`;
    }
}

function renderTimer(label, lastMs, intervalSecs, now) {
    if (!lastMs || lastMs === 0) {
        return `<div class="timer-card">
            <div class="timer-label">${label}</div>
            <div class="timer-value">--:--</div>
            <div class="timer-bar"><div class="timer-bar-fill" style="width:0%"></div></div>
        </div>`;
    }
    const intervalMs = intervalSecs * 1000;
    const elapsed = now - lastMs;
    const remaining = Math.max(0, intervalMs - elapsed);
    const mins = Math.floor(remaining / 60000);
    const secs = Math.floor((remaining % 60000) / 1000);
    const pct = Math.min(100, (elapsed / intervalMs) * 100);
    return `<div class="timer-card">
        <div class="timer-label">${label}</div>
        <div class="timer-value">${mins}:${secs.toString().padStart(2, '0')}</div>
        <div class="timer-bar"><div class="timer-bar-fill" style="width:${pct.toFixed(0)}%"></div></div>
    </div>`;
}

function escapeHtml(str) {
    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function formatTimeAgo(timestampMs) {
    const diff = Date.now() - timestampMs;
    if (diff < 0 || timestampMs === 0) return 'unknown';
    const secs = Math.floor(diff / 1000);
    if (secs < 60) return 'just now';
    const mins = Math.floor(secs / 60);
    if (mins < 60) return `${mins}m ago`;
    const hours = Math.floor(mins / 60);
    if (hours < 24) return `${hours}h ago`;
    const days = Math.floor(hours / 24);
    if (days < 30) return `${days}d ago`;
    return `${Math.floor(days / 30)}mo ago`;
}

// --- Anchor management ---
let currentAnchors = []; // hex node IDs

async function loadMyAnchors() {
    try {
        const [info, anchorPeers] = await Promise.all([
            invoke('get_node_info'),
            invoke('list_anchor_peers'),
        ]);
        currentAnchors = info.anchors || [];

        // Render current anchors
        if (currentAnchors.length === 0) {
            anchorsList.innerHTML = '<p class="empty-hint">No anchors set</p>';
        } else {
            // Get display names for anchor IDs
            anchorsList.innerHTML = currentAnchors.map(aid => {
                const peer = anchorPeers.find(p => p.nodeId === aid);
                const name = peer ? peer.displayName : null;
                const label = escapeHtml(peerLabel(aid, name));
                const icon = generateIdenticon(aid, 18);
                return `<div class="anchor-item">
                    <span class="peer-label">${icon} ${label}</span>
                    <button class="btn btn-danger btn-sm rm-anchor-btn" data-nid="${aid}">Remove</button>
                </div>`;
            }).join('');

            // Attach remove handlers
            anchorsList.querySelectorAll('.rm-anchor-btn').forEach(btn => {
                btn.addEventListener('click', () => doRemoveAnchor(btn.dataset.nid));
            });
        }

        // Populate add dropdown with anchor peers not already in our list
        const available = anchorPeers.filter(p => !currentAnchors.includes(p.nodeId) && p.nodeId !== myNodeId);
        if (available.length === 0) {
            anchorAddSelect.innerHTML = '<option value="">(no anchor peers available)</option>';
        } else {
            anchorAddSelect.innerHTML = available.map(p => {
                const label = p.displayName || p.nodeId.substring(0, 12) + '...';
                return `<option value="${p.nodeId}">${escapeHtml(label)}</option>`;
            }).join('');
        }
    } catch (e) {
        anchorsList.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

async function doAddAnchor() {
    const nid = anchorAddSelect.value;
    if (!nid) { toast('Select an anchor peer'); return; }
    anchorAddBtn.disabled = true;
    try {
        const newAnchors = [...currentAnchors, nid];
        await invoke('set_anchors', { anchors: newAnchors });
        toast('Anchor added');
        loadMyAnchors();
    } catch (e) {
        toast('Error: ' + e);
    } finally {
        anchorAddBtn.disabled = false;
    }
}

async function doRemoveAnchor(nid) {
    try {
        const newAnchors = currentAnchors.filter(a => a !== nid);
        await invoke('set_anchors', { anchors: newAnchors });
        toast('Anchor removed');
        loadMyAnchors();
    } catch (e) {
        toast('Error: ' + e);
    }
}

anchorAddBtn.addEventListener('click', doAddAnchor);

async function loadKnownAnchors() {
    const container = $('#known-anchors-list');
    try {
        const anchors = await invoke('list_known_anchors');
        if (anchors.length === 0) {
            container.innerHTML = '<p class="empty-hint">No discovered anchors yet</p>';
        } else {
            container.innerHTML = anchors.map(a => {
                const icon = generateIdenticon(a.nodeId, 18);
                const label = escapeHtml(peerLabel(a.nodeId, a.displayName));
                const addr = a.addresses.length > 0 ? `<span class="peer-addr">${escapeHtml(a.addresses[0])}</span>` : '';
                return `<div class="anchor-item">
                    <span class="peer-label">${icon} ${label}</span>
                    ${addr}
                </div>`;
            }).join('');
        }
    } catch (e) {
        container.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

// --- Attachment handling ---
function renderAttachmentPreview() {
    if (selectedFiles.length === 0) {
        attachmentPreview.innerHTML = '';
        return;
    }
    attachmentPreview.innerHTML = selectedFiles.map((f, i) => {
        const isImage = f.mime.startsWith('image/');
        const isVideo = f.mime.startsWith('video/');
        if (isImage) {
            const blob = new Blob([f.data], { type: f.mime });
            const url = URL.createObjectURL(blob);
            return `<div class="attach-thumb">
                <img src="${url}" alt="${escapeHtml(f.name)}" />
                <button class="attach-remove" data-idx="${i}" title="Remove">x</button>
            </div>`;
        }
        if (isVideo) {
            const blob = new Blob([f.data], { type: f.mime });
            const url = URL.createObjectURL(blob);
            return `<div class="attach-thumb">
                <video src="${url}#t=0.1" muted preload="auto" width="64" height="64" style="object-fit:cover;border-radius:4px;border:1px solid #444"></video>
                <button class="attach-remove" data-idx="${i}" title="Remove">x</button>
            </div>`;
        }
        return `<div class="attach-thumb">
            <span class="attach-file-name">${escapeHtml(f.name)}</span>
            <button class="attach-remove" data-idx="${i}" title="Remove">x</button>
        </div>`;
    }).join('');
    attachmentPreview.querySelectorAll('.attach-remove').forEach(btn => {
        btn.addEventListener('click', () => {
            selectedFiles.splice(parseInt(btn.dataset.idx), 1);
            renderAttachmentPreview();
        });
    });
}

attachBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
    const maxFiles = 4;
    const maxSize = 10 * 1024 * 1024;
    for (const file of fileInput.files) {
        if (selectedFiles.length >= maxFiles) {
            toast('Max 4 attachments');
            break;
        }
        if (file.size > maxSize) {
            toast(`${file.name} exceeds 10MB limit`);
            continue;
        }
        const reader = new FileReader();
        reader.onload = () => {
            selectedFiles.push({ data: reader.result, mime: file.type || 'application/octet-stream', name: file.name });
            renderAttachmentPreview();
        };
        reader.readAsArrayBuffer(file);
    }
    fileInput.value = '';
});

async function loadBlobAsObjectUrl(cid, postId, mime) {
    const b64 = await invoke('get_blob', { cidHex: cid, postIdHex: postId });
    const binary = atob(b64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
    return URL.createObjectURL(new Blob([bytes], { type: mime }));
}

async function loadPostMedia(container) {
    const imgs = container.querySelectorAll('img[data-cid]');
    for (const img of imgs) {
        const cid = img.dataset.cid;
        const postId = img.dataset.postId;
        const mime = img.dataset.mime || 'image/jpeg';
        try {
            const filePath = await invoke('get_blob_path', { cidHex: cid });
            if (filePath && window.__TAURI__?.core?.convertFileSrc) {
                const assetUrl = window.__TAURI__.core.convertFileSrc(filePath);
                img.onerror = async () => {
                    // Asset protocol failed — fall back to base64 IPC
                    img.onerror = null;
                    try { img.src = await loadBlobAsObjectUrl(cid, postId, mime); }
                    catch (_) { img.alt = 'Image unavailable'; img.classList.add('post-image-missing'); }
                };
                img.src = assetUrl;
            } else {
                img.src = await loadBlobAsObjectUrl(cid, postId, mime);
            }
        } catch (e) {
            img.alt = 'Image unavailable';
            img.classList.add('post-image-missing');
        }
    }
    const vids = container.querySelectorAll('video[data-cid]');
    for (const vid of vids) {
        const cid = vid.dataset.cid;
        const postId = vid.dataset.postId;
        const mime = vid.dataset.mime || 'video/mp4';
        try {
            vid.src = await loadBlobAsObjectUrl(cid, postId, mime);
        } catch (e) {
            vid.poster = '';
            vid.insertAdjacentHTML('afterend', '<span class="empty-hint">Video unavailable</span>');
            vid.remove();
        }
    }
    const audios = container.querySelectorAll('audio[data-cid]');
    for (const aud of audios) {
        const cid = aud.dataset.cid;
        const postId = aud.dataset.postId;
        const mime = aud.dataset.mime || 'audio/mpeg';
        try {
            aud.src = await loadBlobAsObjectUrl(cid, postId, mime);
        } catch (e) {
            aud.insertAdjacentHTML('afterend', '<span class="empty-hint">Audio unavailable</span>');
            aud.remove();
        }
    }
}

// --- File attachment download (PDFs, docs, etc.) ---
document.addEventListener('click', async (e) => {
    const btn = e.target.closest('.file-download');
    if (!btn) return;
    const cid = btn.dataset.cid;
    const postId = btn.dataset.postId;
    const ext = btn.dataset.ext || 'bin';
    const filename = `${cid.slice(0, 8)}.${ext}`;
    const postEl = btn.closest('.post');
    const author = postEl?.querySelector('.post-author')?.textContent?.trim() || 'Unknown';
    showDownloadPrompt(filename, author, cid, postId);
});

function showDownloadPrompt(filename, author, cid, postId) {
    const existing = document.querySelector('.download-prompt-overlay');
    if (existing) existing.remove();

    const overlay = document.createElement('div');
    overlay.className = 'download-prompt-overlay';
    overlay.innerHTML = `
        <div class="download-prompt">
            <h3>Download File</h3>
            <p class="download-filename">${escapeHtml(filename)}</p>
            <p class="download-warning">From: <strong>${escapeHtml(author)}</strong><br>
            Only download files from people you trust.</p>
            <div class="download-prompt-btns">
                <button class="btn btn-secondary download-cancel">Cancel</button>
                <button class="btn btn-primary download-save" data-open="false">Download</button>
                <button class="btn btn-primary download-open" data-open="true">Download &amp; Open</button>
            </div>
        </div>
    `;
    document.body.appendChild(overlay);

    overlay.querySelector('.download-cancel').onclick = () => overlay.remove();
    overlay.onclick = (ev) => { if (ev.target === overlay) overlay.remove(); };

    const doDownload = async (shouldOpen) => {
        overlay.remove();
        try {
            const path = await invoke(shouldOpen ? 'save_and_open_blob' : 'save_blob', {
                cidHex: cid, postIdHex: postId, filename
            });
            toast('Saved to ' + path);
        } catch (err) {
            toast('Download failed: ' + err);
        }
    };
    overlay.querySelector('.download-save').onclick = () => doDownload(false);
    overlay.querySelector('.download-open').onclick = () => doDownload(true);
}

// --- Video speed control ---
document.addEventListener('change', (e) => {
    if (!e.target.classList.contains('video-speed')) return;
    const vid = e.target.closest('.video-wrap')?.querySelector('video');
    if (vid) vid.playbackRate = parseFloat(e.target.value);
});

// --- Video download ---
document.addEventListener('click', async (e) => {
    const btn = e.target.closest('.video-download');
    if (!btn) return;
    const cid = btn.dataset.cid;
    const vid = btn.closest('.video-wrap')?.querySelector('video');
    const postId = vid?.dataset?.postId;
    try {
        const path = await invoke('save_and_open_blob', {
            cidHex: cid, postIdHex: postId, filename: `video_${cid.slice(0, 8)}.mp4`
        });
        toast('Saved to ' + path);
    } catch (_) {
        toast('Download failed');
    }
});

// --- Video expand/collapse (double-click to toggle fullscreen-like view) ---
// --- Image lightbox (click to open full-size popup) ---
document.addEventListener('click', (e) => {
    // Close lightbox on click
    const lb = e.target.closest('.image-lightbox');
    if (lb) { lb.remove(); return; }

    const img = e.target.closest('img.post-image');
    if (!img) return;
    const overlay = document.createElement('div');
    overlay.className = 'image-lightbox';
    const fullImg = document.createElement('img');
    fullImg.src = img.src;
    overlay.appendChild(fullImg);
    document.body.appendChild(overlay);
});

// --- Video expand/collapse (double-click to toggle fullscreen) ---
document.addEventListener('dblclick', (e) => {
    const vid = e.target.closest('video.post-video');
    if (!vid) return;
    vid.classList.toggle('video-expanded');
});
document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
        const lb = document.querySelector('.image-lightbox');
        if (lb) lb.remove();
        const vid = document.querySelector('.video-expanded');
        if (vid) vid.classList.remove('video-expanded');
    }
});

// --- Peer name click handler (toggle bio on People tab) ---
document.addEventListener('click', async (e) => {
    const link = e.target.closest('.peer-name-link');
    if (!link) return;
    e.preventDefault();
    const card = link.closest('.peer-card');
    if (!card) return;
    const bioEl = card.querySelector('.peer-card-bio');
    if (!bioEl) return;
    // Toggle expanded class
    if (bioEl.classList.contains('bio-expanded')) {
        bioEl.classList.remove('bio-expanded');
    } else {
        // Lazy-load bio if not yet loaded
        if (!bioEl.textContent && link.dataset.nodeId) {
            try {
                const info = await invoke('resolve_display', { nodeIdHex: link.dataset.nodeId });
                if (info.bio) {
                    bioEl.textContent = info.bio;
                    bioEl.classList.add('peer-bio');
                } else {
                    bioEl.textContent = '(no bio)';
                    bioEl.classList.add('peer-bio');
                }
            } catch (_) {
                bioEl.textContent = '(no bio)';
                bioEl.classList.add('peer-bio');
            }
        }
        bioEl.classList.add('bio-expanded');
    }
});

// --- Author name click handler (navigate to People tab) ---
document.addEventListener('click', async (e) => {
    const link = e.target.closest('.post-author-link');
    if (!link) return;
    e.preventDefault();
    const nodeId = link.dataset.nodeId;
    // Switch to People tab
    document.querySelector('.tab[data-tab="people"]').click();
    // Scroll to and highlight the peer card after load
    await new Promise(r => setTimeout(r, 300));
    const card = document.querySelector(`.peer-card[data-node-id="${nodeId}"]`);
    if (card) {
        card.scrollIntoView({ behavior: 'smooth', block: 'center' });
        card.classList.add('peer-card-highlight');
        setTimeout(() => card.classList.remove('peer-card-highlight'), 2000);
    }
});

// --- Message peer handler (event delegation) ---
document.addEventListener('click', async (e) => {
    if (!e.target.classList.contains('msg-peer-btn')) return;
    const nodeId = e.target.dataset.nodeId;
    // Switch to Messages tab
    document.querySelector('.tab[data-tab="messages"]').click();
    // Wait for DM recipient options to load, then pre-select
    await loadDmRecipientOptions();
    dmRecipientSelect.value = nodeId;
    dmContent.focus();
});

// --- Delete post handler (event delegation) ---
document.addEventListener('click', async (e) => {
    if (!e.target.classList.contains('delete-post-btn')) return;
    const postId = e.target.dataset.postId;
    if (!confirm('Delete this post? This cannot be undone.')) return;
    e.target.disabled = true;
    try {
        await invoke('delete_post', { postIdHex: postId });
        toast('Post deleted');
        loadFeed(true);
        loadMyPosts(true);
        loadStats();
    } catch (err) {
        toast('Error: ' + err);
    } finally {
        e.target.disabled = false;
    }
});

// --- Engagement handlers (event delegation) ---

// Emoji picker for reactions
const EMOJI_SET = ['👍','❤️','😂','😢','🔥','👏','🎉','💯','🤔','👎'];

document.addEventListener('click', async (e) => {
    // React button → show emoji picker
    if (e.target.classList.contains('react-btn')) {
        e.stopPropagation();
        closeEmojiPicker();
        const postId = e.target.dataset.postId;
        const picker = document.createElement('div');
        picker.className = 'emoji-picker';
        picker.innerHTML = EMOJI_SET.map(em =>
            `<button class="emoji-pick" data-post-id="${postId}" data-emoji="${em}">${em}</button>`
        ).join('');
        e.target.parentElement.appendChild(picker);
        return;
    }

    // Emoji pick → add reaction
    if (e.target.classList.contains('emoji-pick')) {
        const postId = e.target.dataset.postId;
        const emoji = e.target.dataset.emoji;
        closeEmojiPicker();
        try {
            await invoke('react_to_post', { postId, emoji, private: false });
            refreshPostEngagement(postId);
        } catch (err) { toast('Error: ' + err); }
        return;
    }

    // Reaction pill → toggle reaction
    if (e.target.classList.contains('reaction-pill')) {
        const postId = e.target.dataset.postId;
        const emoji = e.target.dataset.emoji;
        try {
            if (e.target.classList.contains('reacted')) {
                await invoke('remove_reaction', { postId, emoji });
            } else {
                await invoke('react_to_post', { postId, emoji, private: false });
            }
            refreshPostEngagement(postId);
        } catch (err) { toast('Error: ' + err); }
        return;
    }

    // Share button → generate share link and copy to clipboard
    if (e.target.classList.contains('share-btn')) {
        const postId = e.target.dataset.postId;
        try {
            const link = await invoke('generate_share_link', { postIdHex: postId });
            if (link) {
                try {
                    await navigator.clipboard.writeText(link);
                    toast('Share link copied!');
                } catch (clipErr) {
                    prompt('Copy your share link:', link);
                }
            } else {
                toast('Only public posts can be shared');
            }
        } catch (err) { toast('Error: ' + err); }
        return;
    }

    // Comment toggle → expand/collapse thread
    if (e.target.classList.contains('comment-toggle-btn')) {
        const postId = e.target.dataset.postId;
        const threadEl = document.getElementById('comments-' + postId);
        if (!threadEl) return;
        if (threadEl.classList.contains('hidden')) {
            threadEl.classList.remove('hidden');
            await loadCommentThread(postId, threadEl);
        } else {
            threadEl.classList.add('hidden');
        }
        return;
    }

    // Comment send button
    if (e.target.classList.contains('comment-send-btn')) {
        const postId = e.target.dataset.postId;
        const input = e.target.parentElement.querySelector('.comment-input');
        const content = input.value.trim();
        if (!content) return;
        e.target.disabled = true;
        try {
            await invoke('comment_on_post', { postId, content });
            input.value = '';
            const threadEl = document.getElementById('comments-' + postId);
            if (threadEl) await loadCommentThread(postId, threadEl);
            refreshPostEngagement(postId);
        } catch (err) { toast('Error: ' + err); }
        finally { e.target.disabled = false; }
        return;
    }

    // Close emoji picker on outside click
    closeEmojiPicker();
});

function closeEmojiPicker() {
    document.querySelectorAll('.emoji-picker').forEach(p => p.remove());
}

async function loadCommentThread(postId, container) {
    try {
        const comments = await invoke('get_comment_thread', { postId });
        let html = '';
        for (const c of comments) {
            const name = c.authorName || c.author.substring(0, 12);
            const time = relativeTime(c.timestampMs);
            html += `<div class="comment-bubble">
                <span class="comment-author">${escapeHtml(name)}</span>
                <span class="comment-text">${escapeHtml(c.content)}</span>
                <span class="comment-time">${time}</span>
            </div>`;
        }
        html += `<div class="comment-compose">
            <input class="comment-input" placeholder="Write a comment..." />
            <button class="btn btn-primary btn-sm comment-send-btn" data-post-id="${postId}">Send</button>
        </div>`;
        container.innerHTML = html;

        // Ctrl+Enter to send
        const input = container.querySelector('.comment-input');
        input.addEventListener('keydown', (ev) => {
            if (ev.key === 'Enter' && ev.ctrlKey) {
                container.querySelector('.comment-send-btn').click();
            }
        });
    } catch (err) {
        container.innerHTML = `<p class="empty-hint">Error loading comments</p>`;
    }
}

async function refreshPostEngagement(postId) {
    try {
        const counts = await invoke('get_reaction_counts', { postId });
        const commentCount = await invoke('get_comments', { postId }).then(c => c.length);
        const postEl = document.querySelector(`.post[data-post-id="${postId}"]`);
        if (!postEl) return;

        // Update reaction pills
        const pillsContainer = postEl.querySelector('.reaction-pills');
        if (pillsContainer) {
            const reactBtn = pillsContainer.querySelector('.react-btn');
            let pillsHtml = counts.map(r =>
                `<button class="reaction-pill${r.reactedByMe ? ' reacted' : ''}" data-post-id="${postId}" data-emoji="${r.emoji}">${r.emoji} ${r.count}</button>`
            ).join('');
            pillsContainer.innerHTML = pillsHtml;
            // Re-add react button
            const btn = document.createElement('button');
            btn.className = 'react-btn';
            btn.dataset.postId = postId;
            btn.title = 'React';
            btn.textContent = '+';
            pillsContainer.appendChild(btn);
        }

        // Update comment count
        const commentBtn = postEl.querySelector('.comment-toggle-btn');
        if (commentBtn) {
            commentBtn.textContent = commentCount > 0 ? `Comment (${commentCount})` : 'Comment';
        }
    } catch (err) {
        // Silently ignore refresh errors
    }
}

// --- Actions ---
async function doPost() {
    const content = postContent.value.trim();
    if (!content && selectedFiles.length === 0) return;
    postBtn.disabled = true;
    try {
        const vis = visibilitySelect.value;
        const params = { content: content || '' };
        if (vis !== 'public') {
            params.visibility = vis;
        }
        if (vis === 'circle') {
            params.circleName = circleSelect.value;
            if (!params.circleName) {
                toast('Select a circle first');
                postBtn.disabled = false;
                return;
            }
        }

        let result;
        if (selectedFiles.length > 0) {
            // Convert ArrayBuffers to base64 strings
            const files = selectedFiles.map(f => {
                const bytes = new Uint8Array(f.data);
                let binary = '';
                for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
                return [btoa(binary), f.mime];
            });
            params.files = files;
            result = await invoke('create_post_with_files', params);
        } else {
            result = await invoke('create_post', params);
        }

        // Set engagement policy if non-default
        const commentPerm = document.getElementById('comment-perm-select').value;
        const reactPerm = document.getElementById('react-perm-select').value;
        if ((commentPerm !== 'public' || reactPerm !== 'both') && result && result.id) {
            try {
                await invoke('set_comment_policy', {
                    postId: result.id,
                    allowComments: commentPerm,
                    allowReacts: reactPerm,
                });
            } catch (_) { /* best effort */ }
        }

        postContent.value = '';
        postContent.style.height = '';
        selectedFiles = [];
        renderAttachmentPreview();
        updateCharCount();
        visibilitySelect.value = 'public';
        updateVisibilityUI();
        toast('Posted!');
        loadFeed(true);
        loadMyPosts(true);
        loadStats();
    } catch (e) {
        toast('Error: ' + e);
    } finally {
        postBtn.disabled = false;
    }
}

async function doSendDM() {
    const recipient = dmRecipientSelect.value;
    const content = dmContent.value.trim();
    if (!recipient) { toast('Select a recipient'); return; }
    if (!content) { toast('Write a message'); return; }
    dmSendBtn.disabled = true;
    try {
        await invoke('create_post', {
            content,
            visibility: 'direct',
            recipientHex: recipient,
        });
        dmContent.value = '';
        toast('Message sent!');
        loadMessages(true);
        loadStats();
    } catch (e) {
        toast('Error: ' + e);
    } finally {
        dmSendBtn.disabled = false;
    }
}

async function doConnect() {
    const cs = connectInput.value.trim();
    if (!cs) return;
    connectBtn.disabled = true;
    connectStatus.textContent = 'Connecting...';
    connectStatus.className = '';
    try {
        const result = await invoke('connect_peer', { connectString: cs });
        connectStatus.textContent = result;
        connectStatus.className = 'status-ok';
        connectInput.value = '';
        loadFollows();
        loadFeed(true);
        loadStats();
    } catch (e) {
        connectStatus.textContent = 'Error: ' + e;
        connectStatus.className = 'status-err';
    } finally {
        connectBtn.disabled = false;
    }
}

async function doSyncAll() {
    syncBtn.disabled = true;
    syncBtn.textContent = 'Syncing...';
    try {
        const result = await invoke('sync_all');
        toast(result);
        loadFeed(true);
        if (currentTab === 'myposts') loadMyPosts(true);
        if (currentTab === 'people') { loadFollows(); }
        if (currentTab === 'messages') loadMessages(true);
        if (currentTab === 'settings') { loadRedundancy(); loadPublicVisible(); if (diagnosticsInterval) loadAllDiagnostics(); }
        loadStats();
    } catch (e) {
        toast('Sync error: ' + e);
    } finally {
        syncBtn.disabled = false;
        syncBtn.textContent = 'Sync Now';
    }
}

async function doSetupName() {
    const name = setupName.value.trim();
    if (!name) return;
    setupBtn.disabled = true;
    try {
        await invoke('set_display_name', { name });
        setupOverlay.classList.add('hidden');
        toast('Welcome, ' + name + '!');
        loadNodeInfo();
    } catch (e) {
        toast('Error: ' + e);
    } finally {
        setupBtn.disabled = false;
    }
}

async function doSaveProfile() {
    const name = profileNameInput.value.trim();
    const bio = profileBioInput.value.trim();
    if (!name) {
        toast('Display name is required');
        return;
    }
    saveProfileBtn.disabled = true;
    try {
        await invoke('set_profile', { name, bio });
        // Also save public_visible setting
        const visible = $('#public-visible-check').checked;
        await invoke('set_public_visible', { visible });
        toast('Profile saved!');
        loadNodeInfo();
    } catch (e) {
        toast('Error: ' + e);
    } finally {
        saveProfileBtn.disabled = false;
    }
}

// --- Profile visibility ---
async function loadPublicVisible() {
    try {
        const visible = await invoke('get_public_visible');
        $('#public-visible-check').checked = visible;
    } catch (e) {
        console.error('loadPublicVisible:', e);
    }
}

// --- Circle profiles ---
async function loadCircleProfiles() {
    const container = $('#circle-profiles-list');
    try {
        const circles = await invoke('list_circles');
        if (circles.length === 0) {
            container.innerHTML = '<p class="empty-hint">Create a circle first in My Posts tab.</p>';
            return;
        }

        let html = '';
        for (const c of circles) {
            let profile = null;
            try {
                profile = await invoke('get_circle_profile', { circleName: c.name });
            } catch (e) { /* no profile yet */ }

            const dn = profile ? profile.displayName : '';
            const bio = profile ? profile.bio : '';

            html += `<div class="circle-profile-card section-card" style="margin-bottom:0.5rem;padding:0.75rem">
                <strong>${escapeHtml(c.name)}</strong>
                <div style="margin-top:0.25rem">
                    <label style="font-size:0.85rem">Display Name</label>
                    <input class="cp-name" data-circle="${escapeHtml(c.name)}" value="${escapeHtml(dn)}" placeholder="Circle display name" maxlength="50" />
                    <label style="font-size:0.85rem">Bio</label>
                    <textarea class="cp-bio" data-circle="${escapeHtml(c.name)}" placeholder="Circle bio" maxlength="200" rows="2">${escapeHtml(bio)}</textarea>
                    <div class="button-row" style="margin-top:0.25rem">
                        <button class="btn btn-primary btn-sm cp-save-btn" data-circle="${escapeHtml(c.name)}">Save</button>
                        ${profile ? `<button class="btn btn-danger btn-sm cp-delete-btn" data-circle="${escapeHtml(c.name)}">Delete</button>` : ''}
                    </div>
                </div>
            </div>`;
        }

        container.innerHTML = html;

        // Attach save handlers
        container.querySelectorAll('.cp-save-btn').forEach(btn => {
            btn.addEventListener('click', async () => {
                const circle = btn.dataset.circle;
                const nameInput = container.querySelector(`.cp-name[data-circle="${circle}"]`);
                const bioInput = container.querySelector(`.cp-bio[data-circle="${circle}"]`);
                const displayName = nameInput.value.trim();
                const bio = bioInput.value.trim();
                if (!displayName) {
                    toast('Display name is required');
                    return;
                }
                try {
                    await invoke('set_circle_profile', { circleName: circle, displayName, bio, avatarCid: null });
                    toast(`Circle profile for "${circle}" saved!`);
                    loadCircleProfiles();
                } catch (e) { toast('Error: ' + e); }
            });
        });

        // Attach delete handlers
        container.querySelectorAll('.cp-delete-btn').forEach(btn => {
            btn.addEventListener('click', async () => {
                const circle = btn.dataset.circle;
                if (!confirm(`Delete circle profile for "${circle}"?`)) return;
                try {
                    await invoke('delete_circle_profile', { circleName: circle });
                    toast(`Circle profile for "${circle}" deleted`);
                    loadCircleProfiles();
                } catch (e) { toast('Error: ' + e); }
            });
        });
    } catch (e) {
        container.innerHTML = '<p class="empty-hint">Error loading circles.</p>';
        console.error('loadCircleProfiles:', e);
    }
}

// --- Visibility UI ---
function updateVisibilityUI() {
    const vis = visibilitySelect.value;
    circleSelect.classList.toggle('hidden', vis !== 'circle');
}

async function loadCircleOptions() {
    try {
        const circles = await invoke('list_circles');
        circleSelect.innerHTML = circles.length === 0
            ? '<option value="">(no circles)</option>'
            : circles.map(c => `<option value="${escapeHtml(c.name)}">${escapeHtml(c.name)} (${c.members.length})</option>`).join('');
    } catch (e) {
        circleSelect.innerHTML = '<option value="">Error</option>';
    }
}

visibilitySelect.addEventListener('change', () => {
    updateVisibilityUI();
    if (visibilitySelect.value === 'circle') loadCircleOptions();
});

// --- Circles management ---
async function loadCircles() {
    try {
        const circles = await invoke('list_circles');
        if (circles.length === 0) {
            circlesList.innerHTML = '<p class="empty-hint">No circles yet. Create one above.</p>';
            return;
        }
        // Also load follows for display names and add-member dropdown
        const follows = await invoke('list_follows');
        const nameMap = {};
        follows.forEach(f => { nameMap[f.nodeId] = f.displayName; });

        circlesList.innerHTML = circles.map(c => {
            const memberHtml = c.members.length === 0
                ? '<span class="empty-hint">No members</span>'
                : c.members.map(mid => {
                    const displayName = nameMap[mid] || (mid.substring(0, 12) + '...');
                    return `<span class="circle-member">${escapeHtml(displayName)} <button class="btn btn-ghost btn-sm rm-member-btn" data-circle="${escapeHtml(c.name)}" data-nid="${mid}">Remove</button></span>`;
                }).join(' ');

            const addOptions = follows
                .filter(f => !c.members.includes(f.nodeId))
                .map(f => {
                    const label = f.displayName || f.nodeId.substring(0, 12) + '...';
                    return `<option value="${f.nodeId}">${escapeHtml(label)}</option>`;
                }).join('');

            return `<div class="circle-card">
                <div class="circle-header">
                    <strong>${escapeHtml(c.name)}</strong> <span class="circle-count">(${c.members.length})</span>
                    <button class="btn btn-danger btn-sm del-circle-btn" data-name="${escapeHtml(c.name)}" style="margin-left:auto">Delete</button>
                </div>
                <div class="circle-members">${memberHtml}</div>
                ${addOptions ? `<div class="circle-add-row">
                    <select class="add-member-select">${addOptions}</select>
                    <button class="btn btn-primary btn-sm add-member-btn" data-circle="${escapeHtml(c.name)}">Add</button>
                </div>` : ''}
            </div>`;
        }).join('');

        // Attach handlers
        circlesList.querySelectorAll('.del-circle-btn').forEach(btn => {
            btn.addEventListener('click', async () => {
                if (!confirm(`Delete circle "${btn.dataset.name}"? This cannot be undone.`)) return;
                try {
                    await invoke('delete_circle', { name: btn.dataset.name });
                    toast('Circle deleted');
                    loadCircles();
                } catch (e) { toast('Error: ' + e); }
            });
        });
        circlesList.querySelectorAll('.rm-member-btn').forEach(btn => {
            btn.addEventListener('click', async () => {
                if (!confirm('Remove this member from the circle?')) return;
                try {
                    await invoke('remove_circle_member', { circleName: btn.dataset.circle, nodeIdHex: btn.dataset.nid });
                    toast('Member removed');
                    loadCircles();
                    // Offer to revoke past access
                    if (confirm('Revoke their access to past circle posts?')) {
                        try {
                            const count = await invoke('revoke_circle_access', {
                                circleName: btn.dataset.circle,
                                nodeIdHex: btn.dataset.nid,
                            });
                            toast(`Revoked access on ${count} posts`);
                        } catch (re) { toast('Revoke error: ' + re); }
                    }
                } catch (e) { toast('Error: ' + e); }
            });
        });
        circlesList.querySelectorAll('.add-member-btn').forEach(btn => {
            btn.addEventListener('click', async () => {
                const select = btn.previousElementSibling;
                if (!select.value) return;
                try {
                    await invoke('add_circle_member', { circleName: btn.dataset.circle, nodeIdHex: select.value });
                    toast('Member added');
                    loadCircles();
                } catch (e) { toast('Error: ' + e); }
            });
        });
    } catch (e) {
        circlesList.innerHTML = `<p class="status-err">Error: ${e}</p>`;
    }
}

createCircleBtn.addEventListener('click', async () => {
    const name = circleNameInput.value.trim();
    if (!name) return;
    createCircleBtn.disabled = true;
    try {
        await invoke('create_circle', { name });
        circleNameInput.value = '';
        toast('Circle created!');
        loadCircles();
    } catch (e) {
        toast('Error: ' + e);
    } finally {
        createCircleBtn.disabled = false;
    }
});
circleNameInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') createCircleBtn.click();
});

// --- Tab switching ---
document.querySelectorAll('.tab').forEach(tab => {
    tab.addEventListener('click', () => {
        if (tab.dataset.tab === currentTab) return;
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        const oldView = document.querySelector('.view.active');
        if (oldView) {
            oldView.classList.remove('active');
            oldView.classList.add('view-exit');
        }
        tab.classList.add('active');
        const target = tab.dataset.tab;
        const newView = document.querySelector(`#view-${target}`);
        // Clear diagnostics auto-refresh when leaving Settings
        if (diagnosticsInterval) { clearInterval(diagnosticsInterval); diagnosticsInterval = null; }
        if (activityInterval) { clearInterval(activityInterval); activityInterval = null; }
        requestAnimationFrame(() => {
            if (oldView) oldView.classList.remove('view-exit');
            newView.classList.add('active');
            currentTab = target;
            if (target === 'feed') {
                if (!feedList.children.length) feedList.innerHTML = renderLoading();
                loadFeed(true);
            }
            if (target === 'myposts') { loadMyPosts(true); loadCircles(); }
            if (target === 'people') {
                if (!followsList.children.length) followsList.innerHTML = renderLoading();
                loadFollows(); loadAudience();
            }
            if (target === 'messages') {
                if (!conversationsList.children.length) conversationsList.innerHTML = renderLoading();
                loadMessages(true); loadDmRecipientOptions();
            }
            if (target === 'settings') { loadRedundancy(); loadPublicVisible(); }
        });
    });
});

// --- Collapsible section toggles ---
$('#circles-toggle').addEventListener('click', () => {
    const body = $('#circles-body');
    body.classList.toggle('hidden');
    $('#circles-toggle').textContent = body.classList.contains('hidden') ? 'Manage Circles' : 'Hide Circles';
});

$('#discover-toggle').addEventListener('click', () => {
    const body = $('#discover-body');
    body.classList.toggle('hidden');
    $('#discover-toggle').textContent = body.classList.contains('hidden') ? 'Discover People' : 'Hide Discover';
    if (!body.classList.contains('hidden')) loadDiscoverPeople();
});

$('#anchors-toggle').addEventListener('click', () => {
    const body = $('#anchors-body');
    body.classList.toggle('hidden');
    $('#anchors-toggle').textContent = body.classList.contains('hidden') ? 'Stored Anchors' : 'Hide Anchors';
    if (!body.classList.contains('hidden')) {
        loadKnownAnchors();
        loadMyAnchors();
    }
});

$('#diagnostics-btn').addEventListener('click', () => {
    const diagHtml = `
        <div id="network-summary"></div>
        <div class="diag-actions">
            <button id="diag-refresh-btn" class="btn btn-ghost btn-sm">Refresh</button>
            <button id="rebalance-btn" class="btn btn-ghost btn-sm">Rebalance Now</button>
            <button id="request-referrals-btn" class="btn btn-ghost btn-sm">Request Referrals</button>
            <span id="diag-update-time" class="diag-timestamp"></span>
        </div>
        <h4 class="subsection-title">Timers</h4>
        <div id="activity-timers" class="diag-grid" style="grid-template-columns: 1fr 1fr;"></div>
        <h4 class="subsection-title">Activity Log</h4>
        <div id="activity-log" class="activity-log-container"></div>
        <h4 class="subsection-title">Mesh Connections</h4>
        <div id="connections-list"></div>
        <h4 class="subsection-title">Known Peers</h4>
        <div id="peers-list"></div>`;
    openPopover('Network Diagnostics', diagHtml, {
        onOpen() {
            // Re-bind dynamic element refs
            networkSummaryEl = $('#network-summary');
            connectionsList = $('#connections-list');
            peersList = $('#peers-list');
            // Wire action buttons
            $('#diag-refresh-btn').addEventListener('click', async () => {
                const btn = $('#diag-refresh-btn');
                btn.disabled = true; btn.textContent = 'Refreshing...';
                try { await loadAllDiagnostics(); toast('Diagnostics refreshed'); }
                catch (e) { toast('Error: ' + e); }
                finally { btn.disabled = false; btn.textContent = 'Refresh'; }
            });
            $('#rebalance-btn').addEventListener('click', async () => {
                const btn = $('#rebalance-btn');
                btn.disabled = true; btn.textContent = 'Rebalancing...';
                try { const r = await invoke('trigger_rebalance'); toast(r); loadAllDiagnostics(); }
                catch (e) { toast('Error: ' + e); }
                finally { btn.disabled = false; btn.textContent = 'Rebalance Now'; }
            });
            $('#request-referrals-btn').addEventListener('click', async () => {
                const btn = $('#request-referrals-btn');
                btn.disabled = true; btn.textContent = 'Requesting...';
                try {
                    const r = await invoke('request_referrals');
                    btn.textContent = r;
                    setTimeout(() => { btn.textContent = 'Request Referrals'; btn.disabled = false; }, 3000);
                    loadAllDiagnostics();
                } catch (e) {
                    btn.textContent = 'Failed';
                    setTimeout(() => { btn.textContent = 'Request Referrals'; btn.disabled = false; }, 3000);
                }
            });
            loadAllDiagnostics();
            diagnosticsInterval = setInterval(loadAllDiagnostics, 10000);
            activityInterval = setInterval(loadActivityLog, 3000);
        }
    });
});

// --- Event handlers ---
postBtn.addEventListener('click', doPost);
postContent.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && e.ctrlKey) doPost();
});
postContent.addEventListener('input', () => {
    autoGrow(postContent);
    updateCharCount();
});
dmSendBtn.addEventListener('click', doSendDM);
dmContent.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && e.ctrlKey) doSendDM();
});
connectBtn.addEventListener('click', doConnect);
connectInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') doConnect();
});
$('#connect-toggle').addEventListener('click', () => {
    const body = $('#connect-body');
    body.classList.toggle('hidden');
    $('#connect-toggle').textContent = body.classList.contains('hidden') ? 'Add peer manually...' : 'Cancel';
});
syncBtn.addEventListener('click', doSyncAll);
copyBtn.addEventListener('click', async () => {
    try {
        await navigator.clipboard.writeText(connectString);
        toast('Connect string copied!');
    } catch (e) {
        console.error('Clipboard write failed:', e);
        prompt('Copy your connect string:', connectString);
    }
});
exportKeyBtn.addEventListener('click', async () => {
    try {
        const key = await invoke('export_identity');
        try {
            await navigator.clipboard.writeText(key);
            toast('Identity key copied to clipboard. KEEP IT SECRET!');
        } catch (clipErr) {
            // Clipboard API may fail in some webview contexts — show the key instead
            console.error('Clipboard write failed:', clipErr);
            prompt('Copy your identity key (KEEP IT SECRET!):', key);
        }
    } catch (e) {
        console.error('export_identity failed:', e);
        toast('Error exporting key: ' + e);
    }
});
saveProfileBtn.addEventListener('click', doSaveProfile);
// Mark profile inputs as touched so we don't overwrite user edits on auto-refresh
profileNameInput.addEventListener('input', () => { profileNameInput.dataset.touched = '1'; });
profileBioInput.addEventListener('input', () => { profileBioInput.dataset.touched = '1'; });
$('#circle-profiles-toggle').addEventListener('click', () => {
    const body = $('#circle-profiles-body');
    body.classList.toggle('hidden');
    $('#circle-profiles-toggle').textContent = body.classList.contains('hidden') ? 'Circle Profiles' : 'Hide Circle Profiles';
    if (!body.classList.contains('hidden')) loadCircleProfiles();
});

// --- Notifications popover ---
$('#notifications-btn').addEventListener('click', async () => {
    // Load current settings
    const msgVal = await invoke('get_setting', { key: 'notif_messages' }).catch(() => null) || 'on';
    const postVal = await invoke('get_setting', { key: 'notif_posts' }).catch(() => null) || 'off';
    const nearbyVal = await invoke('get_setting', { key: 'notif_nearby' }).catch(() => null) || 'on';

    const html = `
        <label for="notif-messages">Messages</label>
        <select id="notif-messages">
            <option value="off"${msgVal === 'off' ? ' selected' : ''}>Off</option>
            <option value="on"${msgVal === 'on' ? ' selected' : ''}>On (no preview)</option>
            <option value="preview"${msgVal === 'preview' ? ' selected' : ''}>On (with preview)</option>
        </select>
        <label for="notif-posts">Posts</label>
        <select id="notif-posts">
            <option value="off"${postVal === 'off' ? ' selected' : ''}>Off</option>
            <option value="follows"${postVal === 'follows' ? ' selected' : ''}>From follows &amp; audience</option>
            <option value="recommended"${postVal === 'recommended' ? ' selected' : ''}>Recommended by follows</option>
            <option value="popular"${postVal === 'popular' ? ' selected' : ''}>Popular (100+)</option>
        </select>
        <label for="notif-nearby">Nearby Users</label>
        <select id="notif-nearby">
            <option value="off"${nearbyVal === 'off' ? ' selected' : ''}>Off</option>
            <option value="on"${nearbyVal === 'on' ? ' selected' : ''}>On</option>
        </select>
        <p class="empty-hint" style="margin-top:0.5rem">Changes are saved automatically.</p>`;

    openPopover('Notifications', html, {
        onOpen() {
            for (const id of ['notif-messages', 'notif-posts', 'notif-nearby']) {
                $(`#${id}`).addEventListener('change', async (e) => {
                    const key = id.replace('-', '_');
                    await invoke('set_setting', { key, value: e.target.value });
                    toast('Setting saved');
                });
            }
        }
    });
});

resetDataBtn.addEventListener('click', async () => {
    if (!confirm('This will delete all posts, peers, and settings. Your identity key will be preserved. Continue?')) return;
    if (!confirm('Are you sure? This cannot be undone.')) return;
    resetDataBtn.disabled = true;
    try {
        const result = await invoke('reset_data');
        toast(result);
        resetDataBtn.textContent = 'Restart app to apply';
    } catch (e) {
        toast('Error: ' + e);
        resetDataBtn.disabled = false;
    }
});

setupBtn.addEventListener('click', doSetupName);
setupName.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') doSetupName();
});

// --- Init ---
async function init() {
    // Backend setup may still be running — retry until state is managed
    for (let attempt = 0; attempt < 30; attempt++) {
        try {
            await invoke('get_node_info');
            break; // backend ready
        } catch (e) {
            if (attempt === 29) { console.error('Backend not ready after 30 attempts'); return; }
            await new Promise(r => setTimeout(r, 300));
        }
    }

    updateCharCount();
    const info = await loadNodeInfo();
    await loadStats();
    await loadFeed();

    // Show setup overlay if no profile exists
    if (info && !info.hasProfile) {
        setupOverlay.classList.remove('hidden');
        setupName.focus();
    }

    // Auto-refresh every 10 seconds (feed, posts, people, stats)
    setInterval(() => {
        if (currentTab === 'feed') loadFeed();
        if (currentTab === 'myposts') loadMyPosts();
        if (currentTab === 'people') { loadFollows(); loadPeers(); loadAudience(); }
        loadStats();
    }, 10000);

    // Tiered DM polling: frequency based on recency of last message
    let _lastMsgPollMs = 0;
    setInterval(() => {
        const now = Date.now();
        const elapsed = now - _lastMsgPollMs;
        const lastMsgAge = now - _lastMsgTimestamp;
        const HOUR = 3600000;
        let interval;
        if (currentTab === 'messages') interval = 5000;        // on messages tab: 5s
        else if (lastMsgAge < 4 * HOUR) interval = 5 * 60000;  // <4h: 5min
        else if (lastMsgAge < 3 * 24 * HOUR) interval = 15 * 60000; // <3d: 15min
        else if (lastMsgAge < 30 * 24 * HOUR) interval = 4 * HOUR;  // <30d: 4h
        else interval = 24 * HOUR;                              // else: daily
        if (elapsed >= interval) {
            _lastMsgPollMs = now;
            loadMessages();
        }
    }, 5000);
}

init();
