// mcp_nostr_explorer — frontend (Phase 3: WebSocket).
//
// Każda karta przeglądarki dostaje sid (sessionStorage) i trzyma jeden
// WebSocket pod /api/ws?sid=…. Kanał jest bidirekcyjny:
//   in:  hello, started, table, assistant_text, error, done, pong, health,
//        pubkey_panel_open, pubkey_panel_update, pubkey_panel_close
//   out: {type:"run", command} | {type:"ping"} | {type:"set_model", model}
//        | {type:"pubkey_panel_close", hex}
// Reconnect z exponential backoff po stronie klienta — sesja na serwerze
// żyje dalej (do GC), więc F5/disconnect nie tracą agenta.

const tablesArea = document.getElementById("tables-area");
const consoleLog = document.getElementById("console-log");
const consoleForm = document.getElementById("console-form");
const commandInput = document.getElementById("command-input");
const clearLogBtn = document.getElementById("clear-log");
const emptyHint = document.querySelector(".empty-hint");
const modelSelect = document.getElementById("model-select");
const modelPopularOnly = document.getElementById("model-popular-only");
const balanceEl = document.getElementById("balance");
const balanceAmountEl = document.getElementById("balance-amount");
const topupBtn = document.getElementById("topup-btn");

let zCounter = 10;
let cardOffset = 0;
let cardCount = 0;
const history = [];
let historyIdx = -1;

// ---- Session ID ----

function getSid() {
    let sid = sessionStorage.getItem("explorer_sid");
    if (!sid) {
        sid = (crypto.randomUUID && crypto.randomUUID()) ||
              `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
        sessionStorage.setItem("explorer_sid", sid);
    }
    return sid;
}

const SID = getSid();

// ---- Bieżący kontekst komendy (do tytułowania kart) ----

let currentCmd = null;     // tekst ostatnio wysłanej komendy
let currentTableIdx = 0;   // licznik tabel w bieżącej turze

// ---- Logowanie ----

function ts() {
    return new Date().toTimeString().slice(0, 8);
}

function logEntry(kind, body) {
    const div = document.createElement("div");
    div.className = `log-entry ${kind}`;
    const tsSpan = document.createElement("span");
    tsSpan.className = "ts";
    tsSpan.textContent = ts();
    const bodySpan = document.createElement("span");
    bodySpan.className = "body";
    bodySpan.textContent = body;
    div.appendChild(tsSpan);
    div.appendChild(bodySpan);
    consoleLog.appendChild(div);
    consoleLog.scrollTop = consoleLog.scrollHeight;
}

// ---- Tabele ----

function isNumericCol(rows, colIdx) {
    let seen = 0;
    for (const r of rows) {
        const v = r[colIdx];
        if (v === null || v === undefined || v === "") continue;
        seen++;
        if (typeof v !== "number" && !(typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v))) {
            return false;
        }
    }
    return seen > 0;
}

function renderTable({ title, columns, rows, meta }) {
    if (emptyHint) emptyHint.style.display = "none";

    const card = document.createElement("div");
    card.className = "table-card";

    const areaRect = tablesArea.getBoundingClientRect();
    const baseLeft = 32 + (cardOffset % 6) * 28;
    const baseTop = 32 + (cardOffset % 6) * 28;
    cardOffset++;
    cardCount++;
    card.style.left = baseLeft + "px";
    card.style.top = baseTop + "px";
    card.style.width = Math.min(720, areaRect.width - baseLeft - 32) + "px";
    card.style.zIndex = ++zCounter;

    const header = document.createElement("div");
    header.className = "card-header";

    const titleEl = document.createElement("div");
    titleEl.className = "card-title";
    titleEl.textContent = title || "(bez tytułu)";

    const metaEl = document.createElement("div");
    metaEl.className = "card-meta mono";
    metaEl.textContent = meta || `${rows.length} wierszy`;

    const closeBtn = document.createElement("button");
    closeBtn.className = "card-close";
    closeBtn.textContent = "×";
    closeBtn.title = "Zamknij";
    closeBtn.addEventListener("click", (e) => {
        e.stopPropagation();
        card.remove();
        cardCount--;
        if (cardCount === 0 && emptyHint) emptyHint.style.display = "";
    });

    header.appendChild(titleEl);
    header.appendChild(metaEl);
    header.appendChild(closeBtn);

    const body = document.createElement("div");
    body.className = "card-body";

    if (!columns || columns.length === 0 || rows.length === 0) {
        body.innerHTML = `<div style="padding: 24px; color: var(--fg-muted); text-align: center;">brak danych</div>`;
    } else {
        const table = document.createElement("table");
        table.className = "data";
        const thead = document.createElement("thead");
        const trh = document.createElement("tr");
        const numericCols = new Set();
        columns.forEach((c, i) => { if (isNumericCol(rows, i)) numericCols.add(i); });
        columns.forEach((col, i) => {
            const th = document.createElement("th");
            th.textContent = col;
            if (numericCols.has(i)) th.classList.add("num");
            trh.appendChild(th);
        });
        thead.appendChild(trh);
        table.appendChild(thead);

        const tbody = document.createElement("tbody");
        for (const row of rows) {
            const tr = document.createElement("tr");
            for (let i = 0; i < columns.length; i++) {
                const td = document.createElement("td");
                const v = row[i];
                td.textContent = v === null || v === undefined ? "" : String(v);
                td.title = td.textContent;
                if (numericCols.has(i)) td.classList.add("num");
                tr.appendChild(td);
            }
            tbody.appendChild(tr);
        }
        table.appendChild(tbody);
        body.appendChild(table);
    }

    card.appendChild(header);
    card.appendChild(body);
    tablesArea.appendChild(card);

    makeDraggable(card, header);
    focusCard(card);
    return card;
}

function focusCard(card) {
    document.querySelectorAll(".table-card.focused").forEach((c) => {
        if (c !== card) c.classList.remove("focused");
    });
    card.classList.add("focused");
    card.style.zIndex = ++zCounter;
}

function makeDraggable(card, handle) {
    let dragging = false;
    let startX = 0, startY = 0, origLeft = 0, origTop = 0;

    card.addEventListener("mousedown", () => focusCard(card));

    handle.addEventListener("mousedown", (e) => {
        if (e.target.closest(".card-close")) return;
        dragging = true;
        startX = e.clientX; startY = e.clientY;
        origLeft = card.offsetLeft; origTop = card.offsetTop;
        document.body.style.cursor = "move";
        e.preventDefault();
    });

    document.addEventListener("mousemove", (e) => {
        if (!dragging) return;
        card.style.left = (origLeft + e.clientX - startX) + "px";
        card.style.top = Math.max(0, origTop + e.clientY - startY) + "px";
    });

    document.addEventListener("mouseup", () => {
        if (dragging) { dragging = false; document.body.style.cursor = ""; }
    });
}

// ---- Pubkey live-panels ----
//
// Tracked-pubkey resource w mcp_nostr_db pcha aktualizacje przy każdym
// evencie dotykającym pubkeya (debounce 2s). Tutaj utrzymujemy mapowanie
// hex → card, żeby `pubkey_panel_update` re-renderował tę samą kartę
// zamiast tworzyć nową.

const pubkeyPanels = new Map();   // hex → HTMLElement

function shortHex(hex) {
    return hex ? hex.slice(0, 8) + "…" + hex.slice(-4) : "";
}

function fmtRelTime(unixSec) {
    if (!unixSec) return "—";
    const ms = Date.now() - unixSec * 1000;
    if (ms < 0) return "w przyszłości";
    const s = Math.floor(ms / 1000);
    if (s < 60) return `${s}s temu`;
    const m = Math.floor(s / 60);
    if (m < 60) return `${m} min temu`;
    const h = Math.floor(m / 60);
    if (h < 48) return `${h} h temu`;
    const d = Math.floor(h / 24);
    return `${d} dni temu`;
}

function fmtRelTimeMs(unixMs) {
    return unixMs ? fmtRelTime(Math.floor(unixMs / 1000)) : "—";
}

function renderPubkeyPanelBody(body, data) {
    body.innerHTML = "";
    if (!data || !data.tracked) {
        const hint = document.createElement("div");
        hint.className = "panel-empty";
        hint.textContent = "pubkey nieobserwowany";
        body.appendChild(hint);
        return;
    }

    const profile = data.profile || {};
    const name = profile.display_name || profile.name || shortHex(data.hex);

    const grid = document.createElement("div");
    grid.className = "panel-grid";

    function row(label, value, opts = {}) {
        const r = document.createElement("div");
        r.className = "panel-row";
        const l = document.createElement("span");
        l.className = "panel-label";
        l.textContent = label;
        const v = document.createElement("span");
        v.className = "panel-value" + (opts.mono ? " mono" : "");
        if (opts.html) v.innerHTML = value; else v.textContent = value;
        r.appendChild(l);
        r.appendChild(v);
        grid.appendChild(r);
    }

    row("name", name);
    row("hex", data.hex, { mono: true });
    row("role", "", { html: `<span class="role-badge role-${data.role}">${data.role}</span>` });
    if (profile.about) row("about", profile.about);
    if (profile.nip05) row("nip05", profile.nip05, { mono: true });
    row("added", fmtRelTimeMs(data.added_at));
    row("last scan", data.last_scan_at ? fmtRelTimeMs(data.last_scan_at) : "(pending)");
    row("limit/relay", data.limit_per_relay ?? "—");
    row("productive relays", (data.productive_relays || []).length === 0
        ? "—"
        : data.productive_relays.join(", "));

    const ev = data.events || {};
    row("authored events", ev.authored ?? 0);
    row("referencing events", ev.referencing ?? 0);
    if (data.role === "primary") row("secondaries from me", data.secondaries_from_me ?? 0);

    body.appendChild(grid);

    if (ev.kinds_top && ev.kinds_top.length) {
        const h = document.createElement("h4");
        h.className = "panel-section-h";
        h.textContent = "kindy (top)";
        body.appendChild(h);
        const kt = document.createElement("table");
        kt.className = "panel-mini-table";
        const tb = document.createElement("tbody");
        for (const k of ev.kinds_top) {
            const tr = document.createElement("tr");
            const tdK = document.createElement("td");
            tdK.textContent = k.kind;
            tdK.className = "num";
            const tdN = document.createElement("td");
            tdN.textContent = k.count;
            tdN.className = "num";
            tr.appendChild(tdK);
            tr.appendChild(tdN);
            tb.appendChild(tr);
        }
        kt.appendChild(tb);
        body.appendChild(kt);
    }

    if (data.last_seen_per_relay && data.last_seen_per_relay.length) {
        const h = document.createElement("h4");
        h.className = "panel-section-h";
        h.textContent = "last seen per relay";
        body.appendChild(h);
        const rt = document.createElement("table");
        rt.className = "panel-mini-table";
        const tb = document.createElement("tbody");
        for (const r of data.last_seen_per_relay) {
            const tr = document.createElement("tr");
            const tdR = document.createElement("td");
            tdR.textContent = r.relay;
            tdR.title = r.relay;
            const tdT = document.createElement("td");
            tdT.textContent = fmtRelTime(r.ts);
            tdT.title = new Date(r.ts * 1000).toISOString();
            tr.appendChild(tdR);
            tr.appendChild(tdT);
            tb.appendChild(tr);
        }
        rt.appendChild(tb);
        body.appendChild(rt);
    }
}

function openPubkeyPanel(hex, data) {
    if (emptyHint) emptyHint.style.display = "none";
    let card = pubkeyPanels.get(hex);
    if (card) {
        // Już otwarta — uczyń ją focused i odśwież treść.
        const body = card.querySelector(".card-body");
        renderPubkeyPanelBody(body, data);
        focusCard(card);
        return;
    }

    card = document.createElement("div");
    card.className = "table-card pubkey-panel-card";

    const areaRect = tablesArea.getBoundingClientRect();
    const baseLeft = 32 + (cardOffset % 6) * 28;
    const baseTop = 32 + (cardOffset % 6) * 28;
    cardOffset++;
    cardCount++;
    card.style.left = baseLeft + "px";
    card.style.top = baseTop + "px";
    card.style.width = Math.min(440, areaRect.width - baseLeft - 32) + "px";
    card.style.zIndex = ++zCounter;

    const header = document.createElement("div");
    header.className = "card-header";

    const titleEl = document.createElement("div");
    titleEl.className = "card-title";
    const name = (data.profile && (data.profile.display_name || data.profile.name)) || shortHex(hex);
    titleEl.textContent = name;
    titleEl.title = hex;

    const metaEl = document.createElement("div");
    metaEl.className = "card-meta mono";
    metaEl.textContent = "live";

    const closeBtn = document.createElement("button");
    closeBtn.className = "card-close";
    closeBtn.textContent = "×";
    closeBtn.title = "Zamknij panel (cancel subscription)";
    closeBtn.addEventListener("click", (e) => {
        e.stopPropagation();
        // Powiadom server, żeby zerwał subskrypcję resource'a — nie ufamy
        // tylko lokalnemu remove(): subscribe leci po stronie serwera i
        // dalej by nas pingował, gdyby user otworzył ten sam pubkey ponownie.
        if (ws && ws.readyState === WebSocket.OPEN) {
            try { ws.send(JSON.stringify({ type: "pubkey_panel_close", hex })); } catch {}
        }
        removePubkeyPanelCard(hex);
    });

    header.appendChild(titleEl);
    header.appendChild(metaEl);
    header.appendChild(closeBtn);

    const body = document.createElement("div");
    body.className = "card-body panel-body";
    renderPubkeyPanelBody(body, data);

    card.appendChild(header);
    card.appendChild(body);
    tablesArea.appendChild(card);
    pubkeyPanels.set(hex, card);

    makeDraggable(card, header);
    focusCard(card);
}

function updatePubkeyPanel(hex, data) {
    const card = pubkeyPanels.get(hex);
    if (!card) return;  // panel został zamknięty — update nas już nie obchodzi
    const body = card.querySelector(".card-body");
    renderPubkeyPanelBody(body, data);
    // krótki flash żeby user zauważył update
    card.classList.remove("panel-flash");
    void card.offsetWidth;
    card.classList.add("panel-flash");
}

function removePubkeyPanelCard(hex) {
    const card = pubkeyPanels.get(hex);
    if (!card) return;
    card.remove();
    pubkeyPanels.delete(hex);
    cardCount--;
    if (cardCount === 0 && emptyHint) emptyHint.style.display = "";
}

// ---- SQL approval modal ----
//
// Server emituje `sql_confirm_request` przed każdym wykonaniem SQL przez
// agenta (run_sql + show_result). Pokazujemy modal z zapytaniem; user
// klika Zatwierdź lub Odrzuć (z opcjonalnym powodem) → wysyłamy
// `sql_confirm_response`. Equeue wielu w jednym czasie nie powinno się
// zdarzać (wrapper jest sekwencyjny per-tura), ale FIFO trzymamy na
// wszelki wypadek.

const sqlConfirmOverlay = document.getElementById("sql-confirm-overlay");
const sqlConfirmCode = document.getElementById("sql-confirm-code");
const sqlConfirmPurpose = document.getElementById("sql-confirm-purpose");
const sqlConfirmReason = document.getElementById("sql-confirm-reason");
const sqlConfirmApproveBtn = document.getElementById("sql-confirm-approve");
const sqlConfirmRejectBtn = document.getElementById("sql-confirm-reject");

const pendingSqlRequests = [];   // FIFO {request_id, sql, purpose}

function purposeLabel(purpose) {
    switch (purpose) {
        case "run_sql": return "diagnostyka (run_sql)";
        case "show_result.table": return "prezentacja: SELECT * z tabeli";
        case "show_result.sql":   return "prezentacja: własny SELECT";
        default: return purpose || "";
    }
}

function showNextSqlRequest() {
    if (pendingSqlRequests.length === 0) {
        sqlConfirmOverlay.hidden = true;
        return;
    }
    const req = pendingSqlRequests[0];
    sqlConfirmCode.textContent = req.sql || "(puste zapytanie?)";
    sqlConfirmPurpose.textContent = purposeLabel(req.purpose);
    sqlConfirmReason.value = "";
    sqlConfirmOverlay.hidden = false;
    // Focus na Zatwierdź — Enter zatwierdza, Esc odrzuca.
    setTimeout(() => sqlConfirmApproveBtn.focus(), 0);
}

function resolveCurrentSqlRequest(approved) {
    const req = pendingSqlRequests.shift();
    if (!req) {
        sqlConfirmOverlay.hidden = true;
        return;
    }
    const payload = {
        type: "sql_confirm_response",
        request_id: req.request_id,
        approved,
    };
    if (!approved) payload.reason = sqlConfirmReason.value.trim();
    try {
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify(payload));
        } else {
            logEntry("err", "WS nie jest gotowy — odpowiedź na zapytanie SQL nie wysłana");
        }
    } catch (err) {
        logEntry("err", `sql_confirm_response: ${err.message}`);
    }
    logEntry(approved ? "info" : "err", approved
        ? `SQL zatwierdzony: ${req.sql.slice(0, 80)}${req.sql.length > 80 ? "…" : ""}`
        : `SQL odrzucony${sqlConfirmReason.value.trim() ? `: ${sqlConfirmReason.value.trim()}` : ""}`);
    showNextSqlRequest();
}

sqlConfirmApproveBtn.addEventListener("click", () => resolveCurrentSqlRequest(true));
sqlConfirmRejectBtn.addEventListener("click",  () => resolveCurrentSqlRequest(false));

// Skróty: Enter = zatwierdź (gdy fokus nie jest w polu reason), Esc = odrzuć.
document.addEventListener("keydown", (e) => {
    if (sqlConfirmOverlay.hidden) return;
    if (e.key === "Escape") {
        e.preventDefault();
        resolveCurrentSqlRequest(false);
    } else if (e.key === "Enter" && document.activeElement !== sqlConfirmReason) {
        e.preventDefault();
        resolveCurrentSqlRequest(true);
    }
});

function enqueueSqlConfirmRequest(req) {
    pendingSqlRequests.push(req);
    if (pendingSqlRequests.length === 1) showNextSqlRequest();
}

// ---- WebSocket ----

let ws = null;
let reconnectTimer = null;
let reconnectDelay = 1000;       // 1s start, exp do RECONNECT_MAX_MS
const RECONNECT_MAX_MS = 30000;
const PING_INTERVAL_MS = 25000;
let pingTimer = null;
let pendingCommand = null;       // wysyłka próbująca się wcisnąć przed openem

function wsUrl() {
    // Buduj URL względem bieżącej lokalizacji — działa pod dowolnym sub-pathem
    // za reverse-proxy (np. https://host/nostr-explorer/), nie tylko pod /.
    const u = new URL("api/ws", location.href);
    u.protocol = location.protocol === "https:" ? "wss:" : "ws:";
    u.searchParams.set("sid", SID);
    return u.toString();
}

function openWS() {
    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
        return;
    }
    try {
        ws = new WebSocket(wsUrl());
    } catch (err) {
        logEntry("err", `nie można otworzyć WS: ${err.message}`);
        scheduleReconnect();
        return;
    }

    ws.addEventListener("open", () => {
        reconnectDelay = 1000;
        // pending command (np. user wysłał coś przed pełnym openem) — flush
        if (pendingCommand !== null) {
            sendRun(pendingCommand);
            pendingCommand = null;
        }
        // ping co N sekund — wykrywa zerwane połączenie zanim TCP zauważy.
        clearInterval(pingTimer);
        pingTimer = setInterval(() => {
            if (ws && ws.readyState === WebSocket.OPEN) {
                try { ws.send(JSON.stringify({ type: "ping" })); } catch {}
            }
        }, PING_INTERVAL_MS);
    });

    ws.addEventListener("message", (ev) => {
        let evt;
        try { evt = JSON.parse(ev.data); }
        catch { logEntry("err", `niepoprawny event: ${ev.data}`); return; }
        handleEvent(evt);
    });

    ws.addEventListener("close", (ev) => {
        clearInterval(pingTimer);
        if (ev.code !== 1000 && ev.code !== 1001) {
            logEntry("err", `WS zamknięty (${ev.code}${ev.reason ? ": " + ev.reason : ""}) — reconnect za ${Math.round(reconnectDelay / 1000)}s`);
        }
        consoleForm.classList.remove("busy");
        scheduleReconnect();
    });

    ws.addEventListener("error", () => {
        // Specyficzny err event nie daje detali — czekamy na close, tam jest kod.
    });
}

function scheduleReconnect() {
    clearTimeout(reconnectTimer);
    reconnectTimer = setTimeout(() => {
        reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
        openWS();
    }, reconnectDelay);
}

function handleEvent(evt) {
    switch (evt.type) {
        case "hello":
            logEntry("info", `stream OK · sid=${evt.sid.slice(0, 8)}… · backend=${evt.backend}`);
            if (evt.backend === "down") {
                logEntry("err", "backend (gateway) niedostępny — komendy zwrócą błąd");
            }
            if (typeof evt.balance_sat === "number") setBalance(evt.balance_sat, null);
            if (evt.model) setActiveModel(evt.model);
            break;
        case "model":
            // serwer potwierdza/broadcastuje zmianę modelu (np. z innej karty)
            setActiveModel(evt.model);
            break;
        case "balance":
            setBalance(evt.sat, evt.last_charge_sat);
            if (typeof evt.last_charge_sat === "number" && evt.last_charge_sat > 0) {
                const usd = typeof evt.last_charge_usd === "number" && evt.last_charge_usd > 0
                    ? ` ≈ $${evt.last_charge_usd.toFixed(6)}`
                    : "";
                logEntry("sys", `naliczono ${fmtSats(evt.last_charge_sat)}${usd} · saldo ${fmtSats(evt.sat)}`);
            }
            break;
        case "started":
            currentCmd = evt.command;
            currentTableIdx = 0;
            consoleForm.classList.add("busy");
            break;
        case "table": {
            currentTableIdx++;
            // Server może podać własny `title` (z `end_turn(show=[{title, ...}])`)
            // — honorujemy go bez zmian. Stary fallback (currentCmd + #idx)
            // zostaje na wypadek gdyby kiedyś coś emitowało tabelę bez tytułu.
            const title = evt.title
                || (currentCmd
                    ? (currentTableIdx > 1 ? `${currentCmd} #${currentTableIdx}` : currentCmd)
                    : `(tabela)`);
            renderTable({
                title,
                columns: evt.table.columns,
                rows: evt.table.rows,
                meta: `${evt.table.rows.length} wierszy`,
            });
            break;
        }
        case "assistant_text":
            if (evt.text) logEntry("info", evt.text);
            break;
        case "error":
            logEntry("err", evt.message || "błąd");
            break;
        case "done": {
            consoleForm.classList.remove("busy");
            const cost = evt.cost_usd ? ` · $${evt.cost_usd.toFixed(6)}` : "";
            logEntry("ok", `done · ${currentTableIdx} tabel · ${evt.took_ms}ms${cost}`);
            currentCmd = null;
            break;
        }
        case "pong":
            // cicho — heartbeat pętli
            break;
        case "health":
            logEntry("info", `backend: ${evt.backend}`);
            break;
        case "pubkey_panel_open":
            openPubkeyPanel(evt.hex, evt.data || {});
            logEntry("info", `panel: ${shortHex(evt.hex)} opened`);
            break;
        case "pubkey_panel_update":
            updatePubkeyPanel(evt.hex, evt.data || {});
            break;
        case "pubkey_panel_close":
            removePubkeyPanelCard(evt.hex);
            break;
        case "sql_confirm_request":
            enqueueSqlConfirmRequest({
                request_id: evt.request_id,
                sql: evt.sql || "",
                purpose: evt.purpose || "",
            });
            break;
        default:
            // nieznany event — zignoruj cicho
    }
}

// ---- Wysyłka komendy ----

function sendRun(command) {
    if (!ws || ws.readyState !== WebSocket.OPEN) {
        // Bufor jednego komendy — gdyby user kliknął Enter w trakcie reconnect.
        pendingCommand = command;
        return;
    }
    try {
        ws.send(JSON.stringify({ type: "run", command }));
    } catch (err) {
        logEntry("err", `błąd send: ${err.message}`);
        consoleForm.classList.remove("busy");
    }
}

function runCommand(command) {
    if (!command.trim()) return;
    history.push(command);
    historyIdx = history.length;
    logEntry("cmd", command);
    sendRun(command);
}

// ---- Balance & model picker ----

function fmtSats(n) {
    return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " sat";
}

function setBalance(sat, lastChargeSat) {
    balanceAmountEl.textContent = fmtSats(sat);
    if (lastChargeSat == null || lastChargeSat === 0) return;
    // ujemne last_charge_sat = topup, dodatnie = obciążenie
    balanceEl.classList.remove("charged", "topup");
    void balanceEl.offsetWidth;  // restart animation
    balanceEl.classList.add(lastChargeSat < 0 ? "topup" : "charged");
}

let allModels = [];      // pełna lista z /api/models
let activeModel = "";    // aktualny model wg serwera (z hello / model event)

function setActiveModel(model) {
    activeModel = model;
    if (modelSelect.options.length && [...modelSelect.options].some(o => o.value === model)) {
        modelSelect.value = model;
    }
}

function renderModelList() {
    const popularOnly = modelPopularOnly.checked;
    const filtered = popularOnly ? allModels.filter(m => m.popular) : allModels;
    modelSelect.innerHTML = "";
    if (!filtered.length) {
        const opt = document.createElement("option");
        opt.textContent = "(brak modeli)";
        opt.disabled = true;
        modelSelect.appendChild(opt);
        modelSelect.disabled = true;
        return;
    }
    let lastOwner = null;
    let group = null;
    for (const m of filtered) {
        if (m.owned_by !== lastOwner) {
            group = document.createElement("optgroup");
            group.label = m.owned_by || "(unknown)";
            modelSelect.appendChild(group);
            lastOwner = m.owned_by;
        }
        const opt = document.createElement("option");
        opt.value = m.id;
        const price = m.pricing && m.pricing.input_per_1M
            ? ` · $${m.pricing.input_per_1M}/$${m.pricing.output_per_1M} per 1M`
            : "";
        opt.textContent = `${m.name}${price}`;
        opt.title = m.id;
        group.appendChild(opt);
    }
    modelSelect.disabled = false;
    if (activeModel && [...modelSelect.options].some(o => o.value === activeModel)) {
        modelSelect.value = activeModel;
    } else if (activeModel) {
        // aktywny model nie pasuje do filtra (np. nie-popularny w popularnym widoku) —
        // wstaw go jako extra opcję, żeby user widział co siedzi w sesji
        const opt = document.createElement("option");
        opt.value = activeModel;
        opt.textContent = `${activeModel} (active)`;
        modelSelect.insertBefore(opt, modelSelect.firstChild);
        modelSelect.value = activeModel;
    }
}

async function loadModels() {
    try {
        const r = await fetch("api/models");
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        const data = await r.json();
        allModels = data.models || [];
        renderModelList();
    } catch (err) {
        logEntry("err", `nie udało się załadować listy modeli: ${err.message}`);
        modelSelect.innerHTML = '<option disabled>(błąd ładowania)</option>';
    }
}

modelPopularOnly.addEventListener("change", renderModelList);

modelSelect.addEventListener("change", () => {
    const chosen = modelSelect.value;
    if (!chosen || chosen === activeModel) return;
    if (!ws || ws.readyState !== WebSocket.OPEN) {
        logEntry("err", "WS nie jest gotowy — model nie zmieniony");
        modelSelect.value = activeModel;
        return;
    }
    ws.send(JSON.stringify({ type: "set_model", model: chosen }));
    logEntry("info", `zmieniam model → ${chosen}`);
});

topupBtn.addEventListener("click", async () => {
    const inp = prompt("Doładuj o ile sat? (symulacja, brak realnego LN)", "1000");
    if (inp == null) return;
    const sats = parseInt(inp, 10);
    if (!Number.isFinite(sats) || sats <= 0) {
        logEntry("err", "topup: niepoprawna liczba sat");
        return;
    }
    try {
        const r = await fetch("api/topup/confirm", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ sid: SID, sats }),
        });
        const data = await r.json();
        if (!r.ok) throw new Error(data.detail || `HTTP ${r.status}`);
        logEntry("ok", `topup +${sats} sat → balance ${data.sat} sat`);
    } catch (err) {
        logEntry("err", `topup nie powiódł się: ${err.message}`);
    }
});

// ---- Bindings ----

consoleForm.addEventListener("submit", (e) => {
    e.preventDefault();
    const cmd = commandInput.value;
    commandInput.value = "";
    runCommand(cmd);
});

commandInput.addEventListener("keydown", (e) => {
    if (e.key === "ArrowUp") {
        if (historyIdx > 0) {
            historyIdx--;
            commandInput.value = history[historyIdx];
            setTimeout(() => commandInput.setSelectionRange(commandInput.value.length, commandInput.value.length), 0);
        }
        e.preventDefault();
    } else if (e.key === "ArrowDown") {
        if (historyIdx < history.length - 1) {
            historyIdx++;
            commandInput.value = history[historyIdx];
        } else {
            historyIdx = history.length;
            commandInput.value = "";
        }
        e.preventDefault();
    }
});

clearLogBtn.addEventListener("click", () => {
    consoleLog.innerHTML = "";
});

window.addEventListener("beforeunload", () => {
    try {
        fetch(`api/session/${encodeURIComponent(SID)}`, {
            method: "DELETE",
            keepalive: true,
        });
    } catch {}
});

// ---- Boot ----

logEntry("info", `gotowe · sid=${SID.slice(0, 8)}…`);
loadModels();
openWS();
